driftseal 2.0.0 → 2.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 +60 -10
- package/README.zh-CN.md +53 -8
- package/bin/driftseal-mcp.js +117 -4
- package/bin/driftseal.js +1026 -199
- package/package.json +1 -1
- package/skills/use-driftseal/SKILL.md +4 -1
package/bin/driftseal.js
CHANGED
|
@@ -43,11 +43,19 @@ const DECISION_STATUSES = [
|
|
|
43
43
|
'superseded',
|
|
44
44
|
];
|
|
45
45
|
const LOG_VERSION = 2;
|
|
46
|
-
const EVENT_SCHEMA_VERSION =
|
|
46
|
+
const EVENT_SCHEMA_VERSION = 2;
|
|
47
|
+
const DEFAULT_WRITE_SCHEMA_VERSION = 1;
|
|
47
48
|
const LEGACY_EVENT_SCHEMA_VERSION = 4;
|
|
48
|
-
const PROTOCOL_VERSION = '2.
|
|
49
|
+
const PROTOCOL_VERSION = '2.1';
|
|
49
50
|
const DEFAULT_LOG_LANGUAGE = 'en';
|
|
51
|
+
const DEFAULT_LANE = 'main';
|
|
52
|
+
const LANE_NAME_RE = /^[a-z][a-z0-9-]{0,62}$/;
|
|
50
53
|
const IN_PROGRESS_GIT_PATH = 'driftseal-v2-in-progress.jsonl';
|
|
54
|
+
const CURRENT_LANE_GIT_PATH = 'driftseal-v2-current-lane';
|
|
55
|
+
const LANE_INDEX_GIT_PATH = 'driftseal-v2-lane-index.json';
|
|
56
|
+
const LANE_INDEX_VERSION = 1;
|
|
57
|
+
const LANE_INDEX_PREFIX_BYTES = 8192;
|
|
58
|
+
const LANE_INDEX_TAIL_BYTES = 64 * 1024;
|
|
51
59
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
52
60
|
const LOCK_INIT_STALE_MS = 5 * 1000;
|
|
53
61
|
const READ_ONLY_NOTICE = '(read-only: another mutation holds the lock; tail repair skipped)';
|
|
@@ -84,7 +92,12 @@ function usageFor(key) {
|
|
|
84
92
|
verify: 'usage: driftseal verify [--allow-tracked-command]',
|
|
85
93
|
end: 'usage: driftseal end [id] [options]',
|
|
86
94
|
status: 'usage: driftseal status',
|
|
87
|
-
log: 'usage: driftseal log [--last N] [--all]',
|
|
95
|
+
log: 'usage: driftseal log [--last N] [--all] [--all-lanes]',
|
|
96
|
+
lane: 'usage: driftseal lane [add|switch|assign|show] ... (run: driftseal help)',
|
|
97
|
+
'lane add': 'usage: driftseal lane add <name> [--desc "<why this capability exists>"]',
|
|
98
|
+
'lane switch': 'usage: driftseal lane switch <name>',
|
|
99
|
+
'lane assign': 'usage: driftseal lane assign <id> <name>',
|
|
100
|
+
'lane show': 'usage: driftseal lane show [name]',
|
|
88
101
|
reclaim:
|
|
89
102
|
'usage: driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]',
|
|
90
103
|
unreclaim: 'usage: driftseal unreclaim <id> --reason "<why>"',
|
|
@@ -265,6 +278,26 @@ function normalizeMigrationSource(value, line) {
|
|
|
265
278
|
};
|
|
266
279
|
}
|
|
267
280
|
|
|
281
|
+
function laneEventId(name) {
|
|
282
|
+
return `lane:${name}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function normalizeLaneName(value, { optional = false, line } = {}) {
|
|
286
|
+
const prefix = line === undefined ? '' : ` on log line ${line}`;
|
|
287
|
+
if (value === undefined || value === null || value === '') {
|
|
288
|
+
if (optional) return DEFAULT_LANE;
|
|
289
|
+
fail(`lane name required${prefix}`);
|
|
290
|
+
}
|
|
291
|
+
if (typeof value !== 'string') fail(`invalid lane name${prefix}`);
|
|
292
|
+
const name = value.trim();
|
|
293
|
+
if (!LANE_NAME_RE.test(name)) {
|
|
294
|
+
fail(
|
|
295
|
+
`invalid lane name "${name}"${prefix} (expected a lowercase letter, then up to 62 letters, digits, or hyphens)`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return name;
|
|
299
|
+
}
|
|
300
|
+
|
|
268
301
|
function normalizeEvent(event, line) {
|
|
269
302
|
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
270
303
|
fail(`invalid event object on log line ${line}`);
|
|
@@ -311,7 +344,8 @@ function normalizeEvent(event, line) {
|
|
|
311
344
|
if (new Set(decisions).size !== decisions.length) {
|
|
312
345
|
fail(`duplicate linked decision on log line ${line}`);
|
|
313
346
|
}
|
|
314
|
-
|
|
347
|
+
const lane = normalizeLaneName(event.lane, { optional: true, line });
|
|
348
|
+
return { ...event, logVersion, outcome, acceptance, decisions, lane, head: normalizeHead(event.head) };
|
|
315
349
|
}
|
|
316
350
|
|
|
317
351
|
if (event.type === 'extend') {
|
|
@@ -412,7 +446,8 @@ function normalizeEvent(event, line) {
|
|
|
412
446
|
typeof source.id !== 'string' || source.id.length === 0)) {
|
|
413
447
|
fail(`invalid imported outcome source on log line ${line}`);
|
|
414
448
|
}
|
|
415
|
-
|
|
449
|
+
const lane = normalizeLaneName(event.lane, { optional: true, line });
|
|
450
|
+
return { ...event, logVersion, decisions, lane, head: normalizeHead(event.head) };
|
|
416
451
|
}
|
|
417
452
|
|
|
418
453
|
if (event.type === 'migration') {
|
|
@@ -432,6 +467,32 @@ function normalizeEvent(event, line) {
|
|
|
432
467
|
};
|
|
433
468
|
}
|
|
434
469
|
|
|
470
|
+
if (event.type === 'lane_add') {
|
|
471
|
+
if (logVersion !== LOG_VERSION) fail(`invalid lane add event on log line ${line}`);
|
|
472
|
+
const lane = normalizeLaneName(event.lane, { line });
|
|
473
|
+
if (lane === DEFAULT_LANE) fail(`cannot add the default lane on log line ${line}`);
|
|
474
|
+
if (event.id !== laneEventId(lane)) {
|
|
475
|
+
fail(`lane add id must be ${laneEventId(lane)} on log line ${line}`);
|
|
476
|
+
}
|
|
477
|
+
if (
|
|
478
|
+
event.description !== undefined &&
|
|
479
|
+
event.description !== null &&
|
|
480
|
+
typeof event.description !== 'string'
|
|
481
|
+
) {
|
|
482
|
+
fail(`invalid lane description on log line ${line}`);
|
|
483
|
+
}
|
|
484
|
+
const description = event.description && event.description.trim().length > 0
|
|
485
|
+
? event.description.trim()
|
|
486
|
+
: null;
|
|
487
|
+
return { ...event, logVersion, lane, description };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (event.type === 'lane_assign') {
|
|
491
|
+
if (logVersion !== LOG_VERSION) fail(`invalid lane assign event on log line ${line}`);
|
|
492
|
+
const lane = normalizeLaneName(event.lane, { line });
|
|
493
|
+
return { ...event, logVersion, lane };
|
|
494
|
+
}
|
|
495
|
+
|
|
435
496
|
if (event.type === 'reclaim' || event.type === 'unreclaim') {
|
|
436
497
|
if (typeof event.reason !== 'string' || event.reason.trim().length === 0) {
|
|
437
498
|
fail(`invalid ${event.type} event on log line ${line}`);
|
|
@@ -519,6 +580,639 @@ function inProgressFile() {
|
|
|
519
580
|
return worktreeInProgressFile();
|
|
520
581
|
}
|
|
521
582
|
|
|
583
|
+
function worktreeMetadataFile(gitPath, cwd = process.cwd()) {
|
|
584
|
+
if (!isGitWorkTree(cwd)) return null;
|
|
585
|
+
const resolved = gitCapture(['rev-parse', '--git-path', gitPath], cwd);
|
|
586
|
+
if (!resolved) return null;
|
|
587
|
+
return path.resolve(cwd, resolved);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function currentLaneFile() {
|
|
591
|
+
if (isParkableOutcomeLog()) return worktreeMetadataFile(CURRENT_LANE_GIT_PATH);
|
|
592
|
+
return path.join(logDir(), '.current-lane');
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function laneIndexFile() {
|
|
596
|
+
if (isParkableOutcomeLog()) return worktreeMetadataFile(LANE_INDEX_GIT_PATH);
|
|
597
|
+
return path.join(logDir(), '.lane-index.json');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function emptyLaneCatalog() {
|
|
601
|
+
return new Map([[DEFAULT_LANE, { name: DEFAULT_LANE, description: null, addedAt: null, head: null }]]);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function readCurrentLaneName() {
|
|
605
|
+
const file = currentLaneFile();
|
|
606
|
+
if (!file || !fs.existsSync(file)) return DEFAULT_LANE;
|
|
607
|
+
const name = fs.readFileSync(file, 'utf8').trim();
|
|
608
|
+
if (!name) return DEFAULT_LANE;
|
|
609
|
+
return normalizeLaneName(name);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function writeCurrentLaneName(name, { readOnly = false } = {}) {
|
|
613
|
+
if (readOnly) return;
|
|
614
|
+
const file = currentLaneFile();
|
|
615
|
+
if (!file) fail('cannot persist the current lane outside a writable seal');
|
|
616
|
+
ensureDirectoryDurable(path.dirname(file));
|
|
617
|
+
ensureDerivedLaneSidecarIgnore();
|
|
618
|
+
atomicWriteFile(file, `${name}\n`, 0o600);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function ensureDerivedLaneSidecarIgnore() {
|
|
622
|
+
if (isParkableOutcomeLog()) return;
|
|
623
|
+
if (!isGitWorkTree(logDir())) return;
|
|
624
|
+
const ignoreFile = path.join(logDir(), '.gitignore');
|
|
625
|
+
let current = fs.existsSync(ignoreFile) ? fs.readFileSync(ignoreFile, 'utf8') : '';
|
|
626
|
+
let next = current;
|
|
627
|
+
for (const name of ['.current-lane', '.lane-index.json']) {
|
|
628
|
+
const present = next.split(/\r?\n/).some((line) => line.trim() === name);
|
|
629
|
+
if (present) continue;
|
|
630
|
+
if (next && !next.endsWith('\n')) next += '\n';
|
|
631
|
+
next += `${name}\n`;
|
|
632
|
+
}
|
|
633
|
+
if (next === current) return;
|
|
634
|
+
ensureDirectoryDurable(logDir());
|
|
635
|
+
atomicWriteFile(ignoreFile, next, 0o644);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function defaultLaneCatalogObject() {
|
|
639
|
+
return { [DEFAULT_LANE]: { name: DEFAULT_LANE, description: null, addedAt: null, head: null } };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function emptyLaneIndexState() {
|
|
643
|
+
return {
|
|
644
|
+
indexVersion: LANE_INDEX_VERSION,
|
|
645
|
+
source: {
|
|
646
|
+
indexedThrough: 0,
|
|
647
|
+
indexedLines: 0,
|
|
648
|
+
prefixHash: contentHash(''),
|
|
649
|
+
tailHash: contentHash(''),
|
|
650
|
+
},
|
|
651
|
+
lastBuild: 'full',
|
|
652
|
+
lanes: emptyLaneCatalog(),
|
|
653
|
+
order: [],
|
|
654
|
+
records: new Map(),
|
|
655
|
+
ranges: new Map(),
|
|
656
|
+
reconciliations: new Map(),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function hashFileRange(file, start, length) {
|
|
661
|
+
if (length <= 0) return contentHash('');
|
|
662
|
+
const fd = fs.openSync(file, 'r');
|
|
663
|
+
try {
|
|
664
|
+
const buf = Buffer.alloc(length);
|
|
665
|
+
const read = fs.readSync(fd, buf, 0, length, start);
|
|
666
|
+
return crypto.createHash('sha256').update(buf.subarray(0, read)).digest('hex');
|
|
667
|
+
} finally {
|
|
668
|
+
fs.closeSync(fd);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function laneIndexSourceIdentity(file, indexedThrough, indexedLines = 0) {
|
|
673
|
+
if (!fs.existsSync(file) || indexedThrough <= 0) {
|
|
674
|
+
return {
|
|
675
|
+
indexedThrough: 0,
|
|
676
|
+
indexedLines: 0,
|
|
677
|
+
prefixHash: contentHash(''),
|
|
678
|
+
tailHash: contentHash(''),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const prefix = Math.min(LANE_INDEX_PREFIX_BYTES, indexedThrough);
|
|
682
|
+
const tail = Math.min(LANE_INDEX_TAIL_BYTES, indexedThrough);
|
|
683
|
+
return {
|
|
684
|
+
indexedThrough,
|
|
685
|
+
indexedLines,
|
|
686
|
+
prefixHash: hashFileRange(file, 0, prefix),
|
|
687
|
+
tailHash: hashFileRange(file, indexedThrough - tail, tail),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function laneIndexMatchesFile(index, file) {
|
|
692
|
+
if (!index || index.indexVersion !== LANE_INDEX_VERSION) return false;
|
|
693
|
+
if (!Number.isSafeInteger(index.source?.indexedLines) || index.source.indexedLines < 0) return false;
|
|
694
|
+
if (!fs.existsSync(file)) return index.source.indexedThrough === 0;
|
|
695
|
+
const size = fs.statSync(file).size;
|
|
696
|
+
if (size < index.source.indexedThrough) return false;
|
|
697
|
+
const identity = laneIndexSourceIdentity(file, index.source.indexedThrough);
|
|
698
|
+
return (
|
|
699
|
+
identity.prefixHash === index.source.prefixHash &&
|
|
700
|
+
identity.tailHash === index.source.tailHash
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function serializeLaneIndex(state) {
|
|
705
|
+
return {
|
|
706
|
+
indexVersion: state.indexVersion,
|
|
707
|
+
source: state.source,
|
|
708
|
+
lastBuild: state.lastBuild,
|
|
709
|
+
lanes: Object.fromEntries(
|
|
710
|
+
[...state.lanes.entries()].map(([name, lane]) => [
|
|
711
|
+
name,
|
|
712
|
+
{ name, description: lane.description || null, addedAt: lane.addedAt || null, head: lane.head || null, inferred: lane.inferred === true },
|
|
713
|
+
])
|
|
714
|
+
),
|
|
715
|
+
order: [...state.order],
|
|
716
|
+
records: Object.fromEntries(state.records),
|
|
717
|
+
ranges: Object.fromEntries(state.ranges),
|
|
718
|
+
reconciliations: Object.fromEntries(state.reconciliations),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function deserializeLaneIndex(raw) {
|
|
723
|
+
if (!raw || raw.indexVersion !== LANE_INDEX_VERSION || !raw.source || !raw.records) return null;
|
|
724
|
+
const lanes = new Map();
|
|
725
|
+
for (const [name, lane] of Object.entries(raw.lanes || defaultLaneCatalogObject())) {
|
|
726
|
+
lanes.set(name, {
|
|
727
|
+
name,
|
|
728
|
+
description: lane.description || null,
|
|
729
|
+
addedAt: lane.addedAt || null,
|
|
730
|
+
head: lane.head || null,
|
|
731
|
+
inferred: lane.inferred === true,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
if (!lanes.has(DEFAULT_LANE)) {
|
|
735
|
+
lanes.set(DEFAULT_LANE, { name: DEFAULT_LANE, description: null, addedAt: null, head: null });
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
indexVersion: LANE_INDEX_VERSION,
|
|
739
|
+
source: raw.source,
|
|
740
|
+
lastBuild: raw.lastBuild || 'full',
|
|
741
|
+
lanes,
|
|
742
|
+
order: Array.isArray(raw.order) ? [...raw.order] : [],
|
|
743
|
+
records: new Map(Object.entries(raw.records)),
|
|
744
|
+
ranges: new Map(Object.entries(raw.ranges || {})),
|
|
745
|
+
reconciliations: new Map(Object.entries(raw.reconciliations || {})),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function cloneLaneIndexState(state) {
|
|
750
|
+
return deserializeLaneIndex(serializeLaneIndex(state));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function linkLaneIndex(state) {
|
|
754
|
+
const heads = new Map();
|
|
755
|
+
for (const id of state.order) {
|
|
756
|
+
const rec = state.records.get(id);
|
|
757
|
+
rec.previous = heads.get(rec.lane) || null;
|
|
758
|
+
heads.set(rec.lane, id);
|
|
759
|
+
}
|
|
760
|
+
for (const [name, lane] of state.lanes) {
|
|
761
|
+
lane.head = heads.get(name) || null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function applyIndexedEvent(state, ev, startByte, endByte) {
|
|
766
|
+
if (ev.type === 'begin' || ev.type === 'import') {
|
|
767
|
+
state.ranges.set(ev.id, { firstByte: startByte, lastByte: endByte });
|
|
768
|
+
} else if (state.records.has(ev.id) || state.ranges.has(ev.id)) {
|
|
769
|
+
const range = state.ranges.get(ev.id) || { firstByte: startByte, lastByte: endByte };
|
|
770
|
+
range.lastByte = endByte;
|
|
771
|
+
state.ranges.set(ev.id, range);
|
|
772
|
+
}
|
|
773
|
+
applyFoldEvent(state, ev);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function applyFoldEvent(state, ev) {
|
|
777
|
+
const { records, reconciliations, order, lanes } = state;
|
|
778
|
+
const ensureLane = (name) => {
|
|
779
|
+
if (lanes.has(name)) return;
|
|
780
|
+
lanes.set(name, { name, description: null, addedAt: null, head: null, inferred: true });
|
|
781
|
+
};
|
|
782
|
+
if (ev.type === 'begin') {
|
|
783
|
+
if (records.has(ev.id)) fail(`duplicate begin event for outcome id: ${ev.id}`);
|
|
784
|
+
const record = newOutcomeRecord(ev);
|
|
785
|
+
ensureLane(record.lane);
|
|
786
|
+
records.set(ev.id, record);
|
|
787
|
+
order.push(ev.id);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (ev.type === 'import') {
|
|
791
|
+
if (records.has(ev.id)) fail(`duplicate imported outcome id: ${ev.id}`);
|
|
792
|
+
const record = newOutcomeRecord({
|
|
793
|
+
...ev,
|
|
794
|
+
ts: ev.beganAt,
|
|
795
|
+
acceptance: [],
|
|
796
|
+
verify: null,
|
|
797
|
+
});
|
|
798
|
+
ensureLane(record.lane);
|
|
799
|
+
record.status = ev.status;
|
|
800
|
+
record.tsEnd = ev.endedAt;
|
|
801
|
+
record.note = ev.summary || null;
|
|
802
|
+
record.reclaimed = ev.reclaimed === true;
|
|
803
|
+
record.reclaimReason = ev.reclaimReason || null;
|
|
804
|
+
record.reclaimedAt = ev.reclaimedAt || null;
|
|
805
|
+
record.imported = {
|
|
806
|
+
sourceIds: ev.sources.map((source) => source.id),
|
|
807
|
+
sourceFingerprint: ev.sourceFingerprint,
|
|
808
|
+
sources: ev.sources,
|
|
809
|
+
};
|
|
810
|
+
records.set(ev.id, record);
|
|
811
|
+
order.push(ev.id);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (ev.type === 'migration') return;
|
|
815
|
+
if (ev.type === 'lane_add') {
|
|
816
|
+
const existing = lanes.get(ev.lane);
|
|
817
|
+
if (existing && !existing.inferred) {
|
|
818
|
+
if (ev.description) existing.description = ev.description;
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
lanes.set(ev.lane, {
|
|
822
|
+
name: ev.lane,
|
|
823
|
+
description: ev.description || null,
|
|
824
|
+
addedAt: ev.ts,
|
|
825
|
+
head: existing ? existing.head : null,
|
|
826
|
+
inferred: false,
|
|
827
|
+
});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (ev.type === 'lane_assign') {
|
|
831
|
+
const rec = records.get(ev.id);
|
|
832
|
+
if (!rec) fail(`lane assign references unknown outcome id: ${ev.id}`);
|
|
833
|
+
if (rec.status === 'in_progress') fail(`cannot assign lane of in_progress outcome ${ev.id}`);
|
|
834
|
+
ensureLane(ev.lane);
|
|
835
|
+
rec.lane = ev.lane;
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (ev.type === 'extend') {
|
|
839
|
+
const rec = records.get(ev.id);
|
|
840
|
+
if (!rec) fail(`extension references unknown outcome id: ${ev.id}`);
|
|
841
|
+
if (rec.status !== 'in_progress') fail(`extension occurred after outcome ${ev.id} was closed`);
|
|
842
|
+
rec.extensions.push({
|
|
843
|
+
extension: ev.extension,
|
|
844
|
+
acceptance: ev.acceptance,
|
|
845
|
+
verify: ev.verify,
|
|
846
|
+
decisions: ev.decisions,
|
|
847
|
+
extendedAt: ev.ts,
|
|
848
|
+
head: ev.head || null,
|
|
849
|
+
});
|
|
850
|
+
rec.acceptance = [...new Set([...rec.acceptance, ...ev.acceptance])];
|
|
851
|
+
if (ev.verify) rec.verify = ev.verify;
|
|
852
|
+
rec.decisions = [...new Set([...rec.decisions, ...ev.decisions])];
|
|
853
|
+
rec.contractHash = outcomeContractHash(rec);
|
|
854
|
+
rec.verification = null;
|
|
855
|
+
rec.decisionUpdates = [];
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (ev.type === 'verify') {
|
|
859
|
+
const rec = records.get(ev.id);
|
|
860
|
+
if (!rec) fail(`verification event references unknown outcome id: ${ev.id}`);
|
|
861
|
+
if (rec.status !== 'in_progress') fail(`verification occurred after outcome ${ev.id} was closed`);
|
|
862
|
+
if (rec.acceptance.length === 0 || !rec.verify) {
|
|
863
|
+
fail(`verification event references outcome ${ev.id} without acceptance criteria`);
|
|
864
|
+
}
|
|
865
|
+
if (ev.command !== rec.verify) fail(`verification command does not match outcome ${ev.id}`);
|
|
866
|
+
if (rec.logVersion === LOG_VERSION && ev.contractHash !== rec.contractHash) {
|
|
867
|
+
fail(`verification contract does not match outcome ${ev.id}`);
|
|
868
|
+
}
|
|
869
|
+
rec.verificationAttempts.push(ev);
|
|
870
|
+
rec.verification = ev;
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (ev.type === 'reclaim' || ev.type === 'unreclaim') {
|
|
874
|
+
const rec = records.get(ev.id);
|
|
875
|
+
if (!rec) fail(`${ev.type} event references unknown outcome id: ${ev.id}`);
|
|
876
|
+
if (ev.type === 'reclaim') {
|
|
877
|
+
if (rec.status === 'in_progress') fail(`cannot reclaim outcome ${ev.id} while it is in_progress`);
|
|
878
|
+
if (rec.reclaimed) fail(`duplicate reclaim event for outcome id: ${ev.id}`);
|
|
879
|
+
rec.reclaimed = true;
|
|
880
|
+
rec.reclaimReason = ev.reason;
|
|
881
|
+
rec.reclaimedAt = ev.ts;
|
|
882
|
+
} else {
|
|
883
|
+
if (!rec.reclaimed) fail(`unreclaim event for outcome id that is not reclaimed: ${ev.id}`);
|
|
884
|
+
rec.reclaimed = false;
|
|
885
|
+
rec.reclaimReason = null;
|
|
886
|
+
rec.reclaimedAt = null;
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (ev.type === 'end') {
|
|
891
|
+
const rec = records.get(ev.id);
|
|
892
|
+
if (!rec) fail(`end event references unknown outcome id: ${ev.id}`);
|
|
893
|
+
if (rec.status !== 'in_progress') fail(`duplicate end event for outcome id: ${ev.id}`);
|
|
894
|
+
const conflictingCancellation = rec.decisionTerminals.find(
|
|
895
|
+
(terminal) => terminal.type === 'decision_reconcile_cancel' && terminal.outcomeStatus !== ev.status
|
|
896
|
+
);
|
|
897
|
+
if (conflictingCancellation) {
|
|
898
|
+
fail(`outcome ${ev.id} was closed as ${ev.status} after reconciliation recovery was cancelled for ${conflictingCancellation.outcomeStatus}`);
|
|
899
|
+
}
|
|
900
|
+
if (
|
|
901
|
+
['completed', 'partial'].includes(ev.status) &&
|
|
902
|
+
rec.decisions.length > 0 &&
|
|
903
|
+
((rec.logVersion === 1 && rec.schemaVersion >= 2 && (ev.schemaVersion || 1) < 2) ||
|
|
904
|
+
rec.decisions.some((decisionId) => qualifyingDecisionUpdates(rec, decisionId).length === 0))
|
|
905
|
+
) {
|
|
906
|
+
fail(`linked outcome ${ev.id} was closed without reconciling every declared decision`);
|
|
907
|
+
}
|
|
908
|
+
if (ev.status === 'completed' && rec.acceptance.length > 0) {
|
|
909
|
+
if (!rec.verification || !rec.verification.passed) {
|
|
910
|
+
fail(`acceptance-bound outcome ${ev.id} was completed without successful machine verification`);
|
|
911
|
+
}
|
|
912
|
+
if (
|
|
913
|
+
(rec.logVersion === 1 && (ev.schemaVersion || 1) < 4) ||
|
|
914
|
+
ev.verificationId !== rec.verification.verificationId ||
|
|
915
|
+
(ev.workspace ?? null) !== rec.verification.workspace ||
|
|
916
|
+
(rec.logVersion === LOG_VERSION &&
|
|
917
|
+
(ev.contractHash !== rec.contractHash || rec.verification.contractHash !== rec.contractHash))
|
|
918
|
+
) {
|
|
919
|
+
fail(`acceptance-bound outcome ${ev.id} was completed with stale machine verification`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
rec.status = ev.status;
|
|
923
|
+
rec.tsEnd = ev.ts;
|
|
924
|
+
rec.note = ev.note || null;
|
|
925
|
+
rec.verifyResult = ev.verifyResult || null;
|
|
926
|
+
rec.endHead = ev.head || null;
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (ev.type === 'decision_reconcile_prepare') {
|
|
930
|
+
const rec = records.get(ev.id);
|
|
931
|
+
if (!rec) fail(`decision reconciliation references unknown outcome id: ${ev.id}`);
|
|
932
|
+
if (rec.status !== 'in_progress') fail(`decision reconciliation occurred after outcome ${ev.id} was closed`);
|
|
933
|
+
if (!rec.decisions.includes(ev.decisionId)) fail(`decision reconciliation references unlinked decision ${ev.decisionId}`);
|
|
934
|
+
if (reconciliations.has(ev.reconciliationId)) fail(`duplicate reconciliation id: ${ev.reconciliationId}`);
|
|
935
|
+
rec.decisionPrepares.push(ev);
|
|
936
|
+
reconciliations.set(ev.reconciliationId, {
|
|
937
|
+
prepare: ev,
|
|
938
|
+
terminal: null,
|
|
939
|
+
contractHash: rec.contractHash,
|
|
940
|
+
});
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (ev.type === 'decision_reconcile') {
|
|
944
|
+
const rec = records.get(ev.id);
|
|
945
|
+
if (!rec) fail(`decision reconciliation references unknown outcome id: ${ev.id}`);
|
|
946
|
+
if (rec.status !== 'in_progress') fail(`decision reconciliation occurred after outcome ${ev.id} was closed`);
|
|
947
|
+
if (rec.logVersion === 1 && rec.schemaVersion >= 2) {
|
|
948
|
+
fail(`linked legacy schema-v2 outcome ${rec.id} contains a legacy decision reconciliation`);
|
|
949
|
+
}
|
|
950
|
+
rec.decisionUpdates.push(ev);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (
|
|
954
|
+
ev.type === 'decision_reconcile_commit' ||
|
|
955
|
+
ev.type === 'decision_reconcile_abort' ||
|
|
956
|
+
ev.type === 'decision_reconcile_cancel'
|
|
957
|
+
) {
|
|
958
|
+
const rec = records.get(ev.id);
|
|
959
|
+
const reconciliation = reconciliations.get(ev.reconciliationId);
|
|
960
|
+
if (rec && rec.status !== 'in_progress') fail(`decision reconciliation occurred after outcome ${ev.id} was closed`);
|
|
961
|
+
if (!rec || !reconciliation || reconciliation.prepare.id !== ev.id || reconciliation.prepare.decisionId !== ev.decisionId) {
|
|
962
|
+
fail(`decision reconciliation terminal has no matching prepare: ${ev.reconciliationId}`);
|
|
963
|
+
}
|
|
964
|
+
if (reconciliation.terminal) fail(`decision reconciliation already has a terminal event: ${ev.reconciliationId}`);
|
|
965
|
+
const priorCancellation = rec.decisionTerminals.find((terminal) => terminal.type === 'decision_reconcile_cancel');
|
|
966
|
+
if (ev.type === 'decision_reconcile_cancel' && priorCancellation && priorCancellation.outcomeStatus !== ev.outcomeStatus) {
|
|
967
|
+
fail(`outcome ${ev.id} has conflicting reconciliation cancellation statuses`);
|
|
968
|
+
}
|
|
969
|
+
if (
|
|
970
|
+
ev.type === 'decision_reconcile_commit' &&
|
|
971
|
+
(reconciliation.prepare.newHash !== ev.fileHash ||
|
|
972
|
+
reconciliation.prepare.fromStatus !== ev.fromStatus ||
|
|
973
|
+
reconciliation.prepare.toStatus !== ev.toStatus)
|
|
974
|
+
) {
|
|
975
|
+
fail(`decision reconciliation commit does not match prepare: ${ev.reconciliationId}`);
|
|
976
|
+
}
|
|
977
|
+
reconciliation.terminal = ev;
|
|
978
|
+
rec.decisionTerminals.push(ev);
|
|
979
|
+
if (
|
|
980
|
+
ev.type === 'decision_reconcile_commit' &&
|
|
981
|
+
reconciliation.contractHash === rec.contractHash
|
|
982
|
+
) {
|
|
983
|
+
rec.decisionUpdates.push(ev);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function consumeLogSlice(file, startByte, onEvent, { repairTail = false, readOnly = false, startLine = 0 } = {}) {
|
|
989
|
+
if (!fs.existsSync(file)) return { endByte: 0, endLine: 0 };
|
|
990
|
+
const fd = fs.openSync(file, 'r');
|
|
991
|
+
let size;
|
|
992
|
+
let buf;
|
|
993
|
+
try {
|
|
994
|
+
size = fs.fstatSync(fd).size;
|
|
995
|
+
if (startByte > size) fail('lane index is ahead of the outcome log');
|
|
996
|
+
if (startByte === size) return { endByte: size, endLine: startLine };
|
|
997
|
+
buf = Buffer.alloc(size - startByte);
|
|
998
|
+
fs.readSync(fd, buf, 0, buf.length, startByte);
|
|
999
|
+
} finally {
|
|
1000
|
+
fs.closeSync(fd);
|
|
1001
|
+
}
|
|
1002
|
+
let text = buf.toString('utf8');
|
|
1003
|
+
if (text.length > 0 && !text.endsWith('\n')) {
|
|
1004
|
+
const rawLines = text.split('\n');
|
|
1005
|
+
const tail = rawLines.at(-1);
|
|
1006
|
+
try {
|
|
1007
|
+
JSON.parse(tail);
|
|
1008
|
+
} catch {
|
|
1009
|
+
const validLength = text.lastIndexOf('\n') + 1;
|
|
1010
|
+
if (!readOnly) {
|
|
1011
|
+
if (!repairTail) fail(`corrupt final log line in ${file}`);
|
|
1012
|
+
const truncateTo = startByte + Buffer.byteLength(text.slice(0, validLength), 'utf8');
|
|
1013
|
+
const repairFd = fs.openSync(file, 'r+');
|
|
1014
|
+
try {
|
|
1015
|
+
fs.ftruncateSync(repairFd, truncateTo);
|
|
1016
|
+
fs.fsyncSync(repairFd);
|
|
1017
|
+
} finally {
|
|
1018
|
+
fs.closeSync(repairFd);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
text = text.slice(0, validLength);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const parts = text.split('\n');
|
|
1025
|
+
let pos = startByte;
|
|
1026
|
+
let lineNumber = startLine;
|
|
1027
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1028
|
+
const line = parts[i];
|
|
1029
|
+
if (i === parts.length - 1 && line === '') break;
|
|
1030
|
+
lineNumber += 1;
|
|
1031
|
+
const start = pos;
|
|
1032
|
+
pos += Buffer.byteLength(line, 'utf8');
|
|
1033
|
+
if (i < parts.length - 1) pos += 1;
|
|
1034
|
+
if (line.trim().length === 0) continue;
|
|
1035
|
+
try {
|
|
1036
|
+
const event = normalizeEvent(JSON.parse(line), lineNumber);
|
|
1037
|
+
if (event.logVersion !== LOG_VERSION) {
|
|
1038
|
+
fail(`v1 intent log cannot be used as a v2 outcome log; run driftseal migrate v1-to-v2 inspect`);
|
|
1039
|
+
}
|
|
1040
|
+
onEvent(event, start, pos);
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
if (err instanceof DriftSealError) throw err;
|
|
1043
|
+
fail(`corrupt log line ${lineNumber} in ${file}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return { endByte: pos, endLine: lineNumber };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function persistLaneIndex(state, { readOnly = false } = {}) {
|
|
1050
|
+
if (readOnly) return;
|
|
1051
|
+
const file = laneIndexFile();
|
|
1052
|
+
if (!file) return;
|
|
1053
|
+
ensureDirectoryDurable(path.dirname(file));
|
|
1054
|
+
ensureDerivedLaneSidecarIgnore();
|
|
1055
|
+
atomicWriteFile(file, `${JSON.stringify(serializeLaneIndex(state))}\n`, 0o600);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function loadPersistedLaneIndex() {
|
|
1059
|
+
const file = laneIndexFile();
|
|
1060
|
+
if (!file || !fs.existsSync(file)) return null;
|
|
1061
|
+
try {
|
|
1062
|
+
return deserializeLaneIndex(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
1063
|
+
} catch {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function syncCommittedLaneIndex({ repairTail = false, readOnly = false } = {}) {
|
|
1069
|
+
const file = logFile();
|
|
1070
|
+
let state = loadPersistedLaneIndex();
|
|
1071
|
+
const canIncrement = state && laneIndexMatchesFile(state, file);
|
|
1072
|
+
let indexedThrough = 0;
|
|
1073
|
+
let indexedLines = 0;
|
|
1074
|
+
if (!canIncrement) {
|
|
1075
|
+
state = emptyLaneIndexState();
|
|
1076
|
+
const slice = consumeLogSlice(file, 0, (event, start, end) => applyIndexedEvent(state, event, start, end), {
|
|
1077
|
+
repairTail,
|
|
1078
|
+
readOnly,
|
|
1079
|
+
});
|
|
1080
|
+
indexedThrough = slice.endByte;
|
|
1081
|
+
indexedLines = slice.endLine;
|
|
1082
|
+
state.lastBuild = 'full';
|
|
1083
|
+
} else {
|
|
1084
|
+
const size = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
1085
|
+
if (size > state.source.indexedThrough) {
|
|
1086
|
+
const slice = consumeLogSlice(
|
|
1087
|
+
file,
|
|
1088
|
+
state.source.indexedThrough,
|
|
1089
|
+
(event, start, end) => applyIndexedEvent(state, event, start, end),
|
|
1090
|
+
{ repairTail, readOnly, startLine: state.source.indexedLines || 0 }
|
|
1091
|
+
);
|
|
1092
|
+
indexedThrough = slice.endByte;
|
|
1093
|
+
indexedLines = slice.endLine;
|
|
1094
|
+
state.lastBuild = 'incremental';
|
|
1095
|
+
} else {
|
|
1096
|
+
indexedThrough = size;
|
|
1097
|
+
indexedLines = state.source.indexedLines || 0;
|
|
1098
|
+
state.lastBuild = 'hot';
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
state.source = laneIndexSourceIdentity(file, indexedThrough, indexedLines);
|
|
1102
|
+
linkLaneIndex(state);
|
|
1103
|
+
if (state.lastBuild !== 'hot') persistLaneIndex(state, { readOnly });
|
|
1104
|
+
return state;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function applyOverlayToLaneIndex(state, overlayEvents) {
|
|
1108
|
+
if (!overlayEvents || overlayEvents.length === 0) return state;
|
|
1109
|
+
const next = cloneLaneIndexState(state);
|
|
1110
|
+
for (const event of overlayEvents) applyFoldEvent(next, event);
|
|
1111
|
+
linkLaneIndex(next);
|
|
1112
|
+
return next;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function foldedRecordsFromLaneIndex(state) {
|
|
1116
|
+
const records = state.order.map((id) => state.records.get(id)).filter(Boolean);
|
|
1117
|
+
records.lanes = state.lanes;
|
|
1118
|
+
return records;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function loadOutcomeView({ repairTail = false, readOnly = false } = {}) {
|
|
1122
|
+
const committed = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
1123
|
+
const park = inProgressFile();
|
|
1124
|
+
if (!park || !fs.existsSync(park)) {
|
|
1125
|
+
return { state: committed, records: foldedRecordsFromLaneIndex(committed) };
|
|
1126
|
+
}
|
|
1127
|
+
try {
|
|
1128
|
+
const committedEvents = readJsonlRecordsFromFile(logFile(), { repairTail, readOnly }).map(
|
|
1129
|
+
(record) => record.event
|
|
1130
|
+
);
|
|
1131
|
+
const overlay = planInProgressOverlay(committedEvents, park, { repairTail, readOnly });
|
|
1132
|
+
if (!overlay || overlay.alreadyCommitted) {
|
|
1133
|
+
if (overlay && overlay.alreadyCommitted && !readOnly) discardInProgressLog(park);
|
|
1134
|
+
return { state: committed, records: foldedRecordsFromLaneIndex(committed) };
|
|
1135
|
+
}
|
|
1136
|
+
if (!readOnly && overlay.mappings.length > 0) writeJsonl(park, overlay.records);
|
|
1137
|
+
const state = applyOverlayToLaneIndex(
|
|
1138
|
+
committed,
|
|
1139
|
+
overlay.records.map((record) => record.event)
|
|
1140
|
+
);
|
|
1141
|
+
return { state, records: foldedRecordsFromLaneIndex(state) };
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
if (readOnly && err && err.code === 'ENOENT') {
|
|
1144
|
+
return { state: committed, records: foldedRecordsFromLaneIndex(committed) };
|
|
1145
|
+
}
|
|
1146
|
+
throw err;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function laneSummary(records, name) {
|
|
1151
|
+
const lanes = records.lanes || emptyLaneCatalog();
|
|
1152
|
+
const lane = lanes.get(name);
|
|
1153
|
+
const members = records.filter((record) => (record.lane || DEFAULT_LANE) === name);
|
|
1154
|
+
const visible = members.filter((record) => !record.reclaimed);
|
|
1155
|
+
return {
|
|
1156
|
+
name,
|
|
1157
|
+
description: lane ? lane.description : null,
|
|
1158
|
+
addedAt: lane ? lane.addedAt : null,
|
|
1159
|
+
inferred: Boolean(lane && lane.inferred),
|
|
1160
|
+
visible: visible.length,
|
|
1161
|
+
count: members.length,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function publicLane(summary) {
|
|
1166
|
+
return {
|
|
1167
|
+
name: summary.name,
|
|
1168
|
+
description: summary.description,
|
|
1169
|
+
addedAt: summary.addedAt || null,
|
|
1170
|
+
inferred: summary.inferred === true,
|
|
1171
|
+
visible: summary.visible,
|
|
1172
|
+
count: summary.count,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function resolveCurrentLane(records, { required = false } = {}) {
|
|
1177
|
+
const requested = readCurrentLaneName();
|
|
1178
|
+
const lanes = records.lanes || emptyLaneCatalog();
|
|
1179
|
+
if (lanes.has(requested)) return { current: requested, missing: null };
|
|
1180
|
+
if (required) {
|
|
1181
|
+
fail(`current lane ${requested} does not exist; switch to ${DEFAULT_LANE} or add it`);
|
|
1182
|
+
}
|
|
1183
|
+
return { current: DEFAULT_LANE, missing: requested };
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function currentLaneOrFail(records) {
|
|
1187
|
+
return resolveCurrentLane(records, { required: true }).current;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function warnMissingCurrentLane(missing) {
|
|
1191
|
+
if (!missing) return;
|
|
1192
|
+
printLine(`warning: current lane ${missing} does not exist; showing ${DEFAULT_LANE}`);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function renderLaneLine(records, name) {
|
|
1196
|
+
const summary = laneSummary(records, name);
|
|
1197
|
+
return `lane: ${name} (${summary.visible} visible / ${summary.count} in lane)`;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function selectLaneLogRecords(records, current) {
|
|
1201
|
+
return records.filter(
|
|
1202
|
+
(record) => (record.lane || DEFAULT_LANE) === current || record.status === 'in_progress'
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function selectLastLogRecords(records, current, n) {
|
|
1207
|
+
const inLane = records.filter((record) => (record.lane || DEFAULT_LANE) === current);
|
|
1208
|
+
const clipped = inLane.slice(-n);
|
|
1209
|
+
const kept = new Set(clipped.map((record) => record.id));
|
|
1210
|
+
const extras = records.filter((record) => record.status === 'in_progress' && !kept.has(record.id));
|
|
1211
|
+
if (extras.length === 0) return clipped;
|
|
1212
|
+
const order = new Map(records.map((record, index) => [record.id, index]));
|
|
1213
|
+
return [...clipped, ...extras].sort((left, right) => order.get(left.id) - order.get(right.id));
|
|
1214
|
+
}
|
|
1215
|
+
|
|
522
1216
|
function liveWorktreeOutcomeLog() {
|
|
523
1217
|
const root = gitWorktreeRoot();
|
|
524
1218
|
if (!root) return null;
|
|
@@ -706,10 +1400,27 @@ function ensureV2OutcomeLogExists() {
|
|
|
706
1400
|
fsyncDirectory(logDir());
|
|
707
1401
|
}
|
|
708
1402
|
|
|
1403
|
+
function eventWriteSchemaVersion(event) {
|
|
1404
|
+
if (Number.isSafeInteger(event.schemaVersion)) return event.schemaVersion;
|
|
1405
|
+
if (event.type === 'lane_add' || event.type === 'lane_assign') return EVENT_SCHEMA_VERSION;
|
|
1406
|
+
if (
|
|
1407
|
+
(event.type === 'begin' || event.type === 'import') &&
|
|
1408
|
+
event.lane &&
|
|
1409
|
+
event.lane !== DEFAULT_LANE
|
|
1410
|
+
) {
|
|
1411
|
+
return EVENT_SCHEMA_VERSION;
|
|
1412
|
+
}
|
|
1413
|
+
return DEFAULT_WRITE_SCHEMA_VERSION;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
709
1416
|
function appendEventTo(file, event) {
|
|
710
1417
|
ensureDirectoryDurable(path.dirname(file));
|
|
711
1418
|
const existed = fs.existsSync(file);
|
|
712
|
-
const storedEvent = {
|
|
1419
|
+
const storedEvent = {
|
|
1420
|
+
logVersion: LOG_VERSION,
|
|
1421
|
+
...event,
|
|
1422
|
+
schemaVersion: eventWriteSchemaVersion(event),
|
|
1423
|
+
};
|
|
713
1424
|
const line = Buffer.from(`${JSON.stringify(storedEvent)}\n`, 'utf8');
|
|
714
1425
|
const fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND, 0o600);
|
|
715
1426
|
try {
|
|
@@ -1182,6 +1893,7 @@ function newOutcomeRecord(ev) {
|
|
|
1182
1893
|
decisions: Array.isArray(ev.decisions) ? ev.decisions : [],
|
|
1183
1894
|
logVersion: ev.logVersion || 1,
|
|
1184
1895
|
schemaVersion: ev.schemaVersion || 1,
|
|
1896
|
+
lane: ev.lane || DEFAULT_LANE,
|
|
1185
1897
|
decisionPrepares: [],
|
|
1186
1898
|
decisionTerminals: [],
|
|
1187
1899
|
decisionUpdates: [],
|
|
@@ -1204,177 +1916,16 @@ function newOutcomeRecord(ev) {
|
|
|
1204
1916
|
|
|
1205
1917
|
/** Fold the event stream into one record per outcome. Legacy v1 events are accepted for migration. */
|
|
1206
1918
|
function fold(events) {
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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));
|
|
1919
|
+
const state = {
|
|
1920
|
+
records: new Map(),
|
|
1921
|
+
reconciliations: new Map(),
|
|
1922
|
+
order: [],
|
|
1923
|
+
lanes: emptyLaneCatalog(),
|
|
1924
|
+
};
|
|
1925
|
+
for (const ev of events) applyFoldEvent(state, ev);
|
|
1926
|
+
const folded = state.order.map((id) => state.records.get(id));
|
|
1927
|
+
folded.lanes = state.lanes;
|
|
1928
|
+
return folded;
|
|
1378
1929
|
}
|
|
1379
1930
|
|
|
1380
1931
|
function qualifyingDecisionUpdates(record, decisionId) {
|
|
@@ -1756,8 +2307,12 @@ function parseArgs(argv, spec, usageKey) {
|
|
|
1756
2307
|
return { positionals, flags };
|
|
1757
2308
|
}
|
|
1758
2309
|
|
|
1759
|
-
function render(rec) {
|
|
2310
|
+
function render(rec, { currentLane } = {}) {
|
|
1760
2311
|
const lines = [`[${rec.id}] ${rec.status}`];
|
|
2312
|
+
const lane = rec.lane || DEFAULT_LANE;
|
|
2313
|
+
if (lane !== DEFAULT_LANE || (currentLane && lane !== currentLane)) {
|
|
2314
|
+
lines.push(` lane: ${lane}`);
|
|
2315
|
+
}
|
|
1761
2316
|
lines.push(` outcome: ${rec.outcome}`);
|
|
1762
2317
|
for (const extension of rec.extensions) lines.push(` extend: ${extension.extension}`);
|
|
1763
2318
|
for (const criterion of rec.acceptance) lines.push(` accept: ${criterion}`);
|
|
@@ -1807,6 +2362,7 @@ function publicOutcome(rec) {
|
|
|
1807
2362
|
return {
|
|
1808
2363
|
id: rec.id,
|
|
1809
2364
|
outcome: rec.outcome,
|
|
2365
|
+
lane: rec.lane || DEFAULT_LANE,
|
|
1810
2366
|
extensions: rec.extensions.map((extension) => ({ ...extension })),
|
|
1811
2367
|
acceptance: [...rec.acceptance],
|
|
1812
2368
|
verify: rec.verify,
|
|
@@ -2085,12 +2641,12 @@ function stripDecisionLogLanguage(block, language = DEFAULT_LOG_LANGUAGE) {
|
|
|
2085
2641
|
function outcomeLogLanguageParagraph(language) {
|
|
2086
2642
|
return `**Log language:** \`${language}\`. Write outcome-log prose (outcome, extension, note,
|
|
2087
2643
|
verify-result, and reclaim/unreclaim reason) in that language. Keep command
|
|
2088
|
-
names, flags, status tokens, and
|
|
2644
|
+
names, flags, status tokens, ids, and lane names in English.`;
|
|
2089
2645
|
}
|
|
2090
2646
|
|
|
2091
|
-
function
|
|
2647
|
+
function intentProtocolBlockV20(language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2092
2648
|
return `${INTENT_PROTOCOL_MARKER}
|
|
2093
|
-
<!-- driftseal-version:
|
|
2649
|
+
<!-- driftseal-version: 2.0 -->
|
|
2094
2650
|
<!-- driftseal-log-language: ${language} -->${localLog ? '\n<!-- driftseal-local-log: true -->' : ''}
|
|
2095
2651
|
|
|
2096
2652
|
## Agent protocol: outcome write-ahead log
|
|
@@ -2099,7 +2655,9 @@ This repository uses DriftSeal (\`driftseal\`) to prevent agent drift. This
|
|
|
2099
2655
|
\`AGENTS.md\` protocol is the source of truth; use the CLI by default, with MCP
|
|
2100
2656
|
and lifecycle hooks as optional adapters.
|
|
2101
2657
|
|
|
2102
|
-
|
|
2658
|
+
**Log language:** \`${language}\`. Write outcome-log prose (outcome, extension, note,
|
|
2659
|
+
verify-result, and reclaim/unreclaim reason) in that language. Keep command
|
|
2660
|
+
names, flags, status tokens, and ids in English.
|
|
2103
2661
|
|
|
2104
2662
|
1. **Write the outcome first**, before changing durable project content:
|
|
2105
2663
|
\`driftseal begin "<coherent delivery outcome>" --accept "<observable result>" --verify "<exact command that proves the cumulative contract>"\`.
|
|
@@ -2141,6 +2699,70 @@ Seal root: \`.seal/\` (override with \`$DRIFTSEAL_HOME\`); outcome log:
|
|
|
2141
2699
|
${INTENT_PROTOCOL_END}`;
|
|
2142
2700
|
}
|
|
2143
2701
|
|
|
2702
|
+
function intentProtocolBlockV21(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2703
|
+
return `${INTENT_PROTOCOL_MARKER}
|
|
2704
|
+
<!-- driftseal-version: ${version} -->
|
|
2705
|
+
<!-- driftseal-log-language: ${language} -->${localLog ? '\n<!-- driftseal-local-log: true -->' : ''}
|
|
2706
|
+
|
|
2707
|
+
## Agent protocol: outcome write-ahead log
|
|
2708
|
+
|
|
2709
|
+
This repository uses DriftSeal (\`driftseal\`) to prevent agent drift. This
|
|
2710
|
+
\`AGENTS.md\` protocol is the source of truth; use the CLI by default, with MCP
|
|
2711
|
+
and lifecycle hooks as optional adapters.
|
|
2712
|
+
|
|
2713
|
+
${outcomeLogLanguageParagraph(language)}
|
|
2714
|
+
|
|
2715
|
+
1. **Write the outcome first**, before changing durable project content:
|
|
2716
|
+
\`driftseal begin "<coherent delivery outcome>" --accept "<observable result>" --verify "<exact command that proves the cumulative contract>"\`.
|
|
2717
|
+
Repeat \`--accept\` for independently observable criteria and add one
|
|
2718
|
+
\`--decision <id>\` for each existing MADR this outcome may change.
|
|
2719
|
+
Record outcomes for changes intended to persist in the project: code,
|
|
2720
|
+
configuration, documentation, dependencies, and equivalent files, inside or
|
|
2721
|
+
outside Git. Git operations, checks, temporary auxiliary work, and external
|
|
2722
|
+
state changes are exempt when they do not write durable project content here.
|
|
2723
|
+
2. **Extend only the same outcome.** For another step toward the same coherent
|
|
2724
|
+
delivery goal, append \`driftseal extend "<addition>"\`. It may add
|
|
2725
|
+
\`--accept\`, \`--decision\`, and a replacement \`--verify\`; adding acceptance
|
|
2726
|
+
requires a replacement verifier that proves the complete accumulated contract.
|
|
2727
|
+
Every extension invalidates earlier verification and MADR reconciliation. If
|
|
2728
|
+
the delivery goal changes, close the current outcome honestly and begin a new one.
|
|
2729
|
+
One open outcome belongs to one worktree, or one configured non-Git project
|
|
2730
|
+
root. Every agent changing durable content in the same root re-anchors and
|
|
2731
|
+
continues it; separate worktrees hold separate outcomes.
|
|
2732
|
+
Outcomes belong to one named lane (\`driftseal lane\`). The default lane is
|
|
2733
|
+
\`main\`; untagged history lives there. Re-anchoring and \`driftseal log\`
|
|
2734
|
+
follow the current lane. Close the open outcome before switching lanes.
|
|
2735
|
+
Create a lane only for a long-lived capability you expect to leave and resume.
|
|
2736
|
+
3. **Reconcile, verify, then close.** After the final extension, reconcile every
|
|
2737
|
+
linked MADR with \`driftseal decision update\`. Inspect \`driftseal status\`,
|
|
2738
|
+
then run \`driftseal verify\` for an acceptance-bound outcome. A verifier
|
|
2739
|
+
without matching local provenance is untrusted and requires
|
|
2740
|
+
\`--allow-tracked-command\` after inspection. Finish with
|
|
2741
|
+
\`driftseal end -s completed|partial|failed|abandoned -n "<what happened>"\`.
|
|
2742
|
+
Completed outcomes require fresh successful verification bound to both the
|
|
2743
|
+
current contract hash and Git-visible workspace. Never report success without
|
|
2744
|
+
closing the outcome.
|
|
2745
|
+
4. **Re-anchor after context loss or handoff:** run \`driftseal status\` and
|
|
2746
|
+
\`driftseal log --last 3\` before changing durable content. Both follow the
|
|
2747
|
+
current lane. Resume the open outcome when it still matches; otherwise close
|
|
2748
|
+
it and begin a new one. If the requested work belongs to a different existing
|
|
2749
|
+
lane, switch first.
|
|
2750
|
+
|
|
2751
|
+
**Log access goes only through DriftSeal.** Never read, edit, move, or delete
|
|
2752
|
+
\`.seal/outcomes/events.jsonl\` (or its configured equivalent) directly. Use
|
|
2753
|
+
\`reclaim\`/\`unreclaim\` for visibility markers and \`absorb\` after merge
|
|
2754
|
+
collisions. These operations preserve append-only single-lineage history.
|
|
2755
|
+
|
|
2756
|
+
Seal root: \`.seal/\` (override with \`$DRIFTSEAL_HOME\`); outcome log:
|
|
2757
|
+
\`.seal/outcomes/events.jsonl\`; ${localLog ? 'keep `.seal/` local and untracked.' : 'commit `.seal/` with the code.'}
|
|
2758
|
+
${INTENT_PROTOCOL_END}`;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
function intentProtocolBlock(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2762
|
+
if (String(version) === '2.0') return intentProtocolBlockV20(language, localLog);
|
|
2763
|
+
return intentProtocolBlockV21(version, language, localLog);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2144
2766
|
function v1IntentProtocolBlock(version = 14, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2145
2767
|
return `${INTENT_PROTOCOL_MARKER}
|
|
2146
2768
|
<!-- driftseal-version: ${version} -->
|
|
@@ -2858,6 +3480,8 @@ const SKILL_RELEASE_DIGESTS = new Set([
|
|
|
2858
3480
|
'72ddea79940bdf2bce66d491888f11423ae1bd383e1b511028fda617e6f6fb27', // f395778 1.1.6 parked intents
|
|
2859
3481
|
'df8bc7035de1a19faf307c92f9bb0f4052e683d1a94881c2c5d5cbef48b67568', // dc9899d 1.1.7 parked intents in absorb
|
|
2860
3482
|
'42a0549dff21483c0508ea4a79658e7bf05cd98f8af4238c95dbd23cdcde7ee6', // 2.0.0 outcome workflow
|
|
3483
|
+
'38e89060ff37ecdd663eae73a3e0c646d6bb220fc8232a5762356375d84ba69b', // 2.1.0 outcome lanes
|
|
3484
|
+
'b4b2cc27ea71c5777b5eb9b67d861fbe1da43f31ab5d422d6a877e0cad592493', // 2.1.0 lane re-anchor recovery
|
|
2861
3485
|
]);
|
|
2862
3486
|
|
|
2863
3487
|
function skillInstallUsage() {
|
|
@@ -3849,6 +4473,19 @@ function rebindV2ContractHashes(records) {
|
|
|
3849
4473
|
});
|
|
3850
4474
|
}
|
|
3851
4475
|
|
|
4476
|
+
function dropDuplicateLaneAdds(oursEvents, theirsRecords) {
|
|
4477
|
+
const seen = new Set();
|
|
4478
|
+
for (const event of oursEvents) {
|
|
4479
|
+
if (event.type === 'lane_add') seen.add(event.lane);
|
|
4480
|
+
}
|
|
4481
|
+
return theirsRecords.filter((record) => {
|
|
4482
|
+
if (record.event.type !== 'lane_add') return true;
|
|
4483
|
+
if (seen.has(record.event.lane)) return false;
|
|
4484
|
+
seen.add(record.event.lane);
|
|
4485
|
+
return true;
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
|
|
3852
4489
|
function remapTheirsRecords(theirsNew, oursUsedEvents, decisionMap, hashMap = new Map()) {
|
|
3853
4490
|
const intentMap = new Map();
|
|
3854
4491
|
const mappings = [];
|
|
@@ -3870,6 +4507,7 @@ function remapTheirsRecords(theirsNew, oursUsedEvents, decisionMap, hashMap = ne
|
|
|
3870
4507
|
|
|
3871
4508
|
function repairDuplicateOutcomeRecords(records, decisionMap, hashMap = new Map()) {
|
|
3872
4509
|
const seenBegins = new Set();
|
|
4510
|
+
const seenLanes = new Set();
|
|
3873
4511
|
const intentMap = new Map();
|
|
3874
4512
|
const used = [];
|
|
3875
4513
|
const mappings = [];
|
|
@@ -3877,6 +4515,10 @@ function repairDuplicateOutcomeRecords(records, decisionMap, hashMap = new Map()
|
|
|
3877
4515
|
let incomingSide = false;
|
|
3878
4516
|
for (const record of records) {
|
|
3879
4517
|
let event = record.event;
|
|
4518
|
+
if (event.type === 'lane_add') {
|
|
4519
|
+
if (seenLanes.has(event.lane)) continue;
|
|
4520
|
+
seenLanes.add(event.lane);
|
|
4521
|
+
}
|
|
3880
4522
|
if (isOutcomeStart(event) && seenBegins.has(event.id)) {
|
|
3881
4523
|
incomingSide = true;
|
|
3882
4524
|
const { date } = parseOutcomeId(event.id);
|
|
@@ -3946,7 +4588,7 @@ function abandonOpenIntent(records, targetId, side) {
|
|
|
3946
4588
|
records.push({
|
|
3947
4589
|
event: {
|
|
3948
4590
|
logVersion: LOG_VERSION,
|
|
3949
|
-
schemaVersion:
|
|
4591
|
+
schemaVersion: DEFAULT_WRITE_SCHEMA_VERSION,
|
|
3950
4592
|
type: 'end',
|
|
3951
4593
|
id: targetId,
|
|
3952
4594
|
ts: new Date().toISOString(),
|
|
@@ -4169,7 +4811,10 @@ function absorbFromStreams(ours, theirs, baseRecords, options) {
|
|
|
4169
4811
|
baseIds: options.baseDecisionIds || new Set(),
|
|
4170
4812
|
});
|
|
4171
4813
|
const remapped = remapTheirsRecords(
|
|
4172
|
-
|
|
4814
|
+
dropDuplicateLaneAdds(
|
|
4815
|
+
[...streams.base, ...streams.oursNew].map((record) => record.event),
|
|
4816
|
+
streams.theirsNew
|
|
4817
|
+
),
|
|
4173
4818
|
[...streams.base, ...streams.oursNew].map((record) => record.event),
|
|
4174
4819
|
decisionPlan.decisionMap,
|
|
4175
4820
|
decisionPlan.hashMap
|
|
@@ -4913,7 +5558,7 @@ function validateMigrationPlan(plan, snapshot) {
|
|
|
4913
5558
|
}
|
|
4914
5559
|
|
|
4915
5560
|
function storedV2Event(event) {
|
|
4916
|
-
return { logVersion: LOG_VERSION, schemaVersion:
|
|
5561
|
+
return { logVersion: LOG_VERSION, schemaVersion: DEFAULT_WRITE_SCHEMA_VERSION, ...event };
|
|
4917
5562
|
}
|
|
4918
5563
|
|
|
4919
5564
|
function migrationImportEvent(snapshot, validated, group, events) {
|
|
@@ -5270,6 +5915,116 @@ function checkMigration(snapshot, { sourceMissing = false } = {}) {
|
|
|
5270
5915
|
};
|
|
5271
5916
|
}
|
|
5272
5917
|
|
|
5918
|
+
function listLaneSnapshot(records) {
|
|
5919
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
5920
|
+
const { current, missing } = resolveCurrentLane(records);
|
|
5921
|
+
const lanes = [...catalog.keys()].map((name) => {
|
|
5922
|
+
const summary = laneSummary(records, name);
|
|
5923
|
+
return {
|
|
5924
|
+
...publicLane(summary),
|
|
5925
|
+
current: name === current,
|
|
5926
|
+
};
|
|
5927
|
+
});
|
|
5928
|
+
return { current, missingCurrentLane: missing, lanes, total: records.length };
|
|
5929
|
+
}
|
|
5930
|
+
|
|
5931
|
+
function printLaneSnapshot(snapshot) {
|
|
5932
|
+
warnMissingCurrentLane(snapshot.missingCurrentLane);
|
|
5933
|
+
const lines = snapshot.lanes.map((lane) => {
|
|
5934
|
+
const mark = lane.current ? '*' : ' ';
|
|
5935
|
+
const desc = lane.description ? ` — ${lane.description}` : '';
|
|
5936
|
+
const inferred = lane.inferred ? ' (inferred)' : '';
|
|
5937
|
+
return `${mark} ${lane.name} ${lane.visible} visible / ${lane.count} in lane${desc}${inferred}`;
|
|
5938
|
+
});
|
|
5939
|
+
printLine(`current lane: ${snapshot.current}`);
|
|
5940
|
+
printLine(lines.join('\n'));
|
|
5941
|
+
return snapshot;
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5944
|
+
function showLanes(argv, { readOnly = false } = {}) {
|
|
5945
|
+
const { positionals } = parseArgs(argv, {}, 'lane show');
|
|
5946
|
+
const view = loadOutcomeView({ repairTail: true, readOnly });
|
|
5947
|
+
const snapshot = listLaneSnapshot(view.records);
|
|
5948
|
+
if (positionals.length === 0) return printLaneSnapshot(snapshot);
|
|
5949
|
+
if (positionals.length !== 1) fail(usageFor('lane show'));
|
|
5950
|
+
const name = normalizeLaneName(positionals[0]);
|
|
5951
|
+
const lane = snapshot.lanes.find((item) => item.name === name);
|
|
5952
|
+
if (!lane) fail(`unknown lane ${name}`);
|
|
5953
|
+
printLine(
|
|
5954
|
+
`${lane.current ? 'current ' : ''}lane: ${lane.name} (${lane.visible} visible / ${lane.count} in lane)`
|
|
5955
|
+
);
|
|
5956
|
+
if (lane.description) printLine(lane.description);
|
|
5957
|
+
return lane;
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5960
|
+
function addLane(argv) {
|
|
5961
|
+
const { positionals, flags } = parseArgs(argv, { desc: 'single' }, 'lane add');
|
|
5962
|
+
if (positionals.length !== 1) fail(usageFor('lane add'));
|
|
5963
|
+
const name = normalizeLaneName(positionals[0]);
|
|
5964
|
+
if (name === DEFAULT_LANE) fail(`lane ${DEFAULT_LANE} always exists`);
|
|
5965
|
+
const events = readEvents({ repairTail: true });
|
|
5966
|
+
const records = fold(events);
|
|
5967
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
5968
|
+
const existing = catalog.get(name);
|
|
5969
|
+
if (existing && !existing.inferred) fail(`lane ${name} already exists`);
|
|
5970
|
+
const description = flags.desc && flags.desc.trim() ? flags.desc.trim() : null;
|
|
5971
|
+
appendEvent({
|
|
5972
|
+
type: 'lane_add',
|
|
5973
|
+
id: laneEventId(name),
|
|
5974
|
+
ts: new Date().toISOString(),
|
|
5975
|
+
lane: name,
|
|
5976
|
+
description,
|
|
5977
|
+
});
|
|
5978
|
+
printLine(`added lane ${name}`);
|
|
5979
|
+
return { name, description };
|
|
5980
|
+
}
|
|
5981
|
+
|
|
5982
|
+
function switchLane(argv) {
|
|
5983
|
+
const { positionals } = parseArgs(argv, {}, 'lane switch');
|
|
5984
|
+
if (positionals.length !== 1) fail(usageFor('lane switch'));
|
|
5985
|
+
const name = normalizeLaneName(positionals[0]);
|
|
5986
|
+
const events = readEvents({ repairTail: true });
|
|
5987
|
+
const records = fold(events);
|
|
5988
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
5989
|
+
if (!catalog.has(name)) fail(`unknown lane ${name}; add it with driftseal lane add ${name}`);
|
|
5990
|
+
const open = openOutcome(records);
|
|
5991
|
+
if (open) {
|
|
5992
|
+
fail(
|
|
5993
|
+
`outcome ${open.id} is still in_progress on lane ${open.lane || DEFAULT_LANE}; ` +
|
|
5994
|
+
'end it before switching lanes'
|
|
5995
|
+
);
|
|
5996
|
+
}
|
|
5997
|
+
writeCurrentLaneName(name);
|
|
5998
|
+
printLine(`switched to lane ${name}`);
|
|
5999
|
+
return { current: name };
|
|
6000
|
+
}
|
|
6001
|
+
|
|
6002
|
+
function assignLane(argv) {
|
|
6003
|
+
const { positionals } = parseArgs(argv, {}, 'lane assign');
|
|
6004
|
+
if (positionals.length !== 2) fail(usageFor('lane assign'));
|
|
6005
|
+
const id = positionals[0];
|
|
6006
|
+
const name = normalizeLaneName(positionals[1]);
|
|
6007
|
+
const events = readEvents({ repairTail: true });
|
|
6008
|
+
const records = fold(events);
|
|
6009
|
+
const record = records.find((candidate) => candidate.id === id);
|
|
6010
|
+
if (!record) fail(`unknown outcome id: ${id}`);
|
|
6011
|
+
if (record.status === 'in_progress') fail(`cannot assign lane of in_progress outcome ${id}`);
|
|
6012
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
6013
|
+
if (!catalog.has(name)) fail(`unknown lane ${name}; add it with driftseal lane add ${name}`);
|
|
6014
|
+
if ((record.lane || DEFAULT_LANE) === name) {
|
|
6015
|
+
printLine(`${id} already on lane ${name}`);
|
|
6016
|
+
return publicOutcome(record);
|
|
6017
|
+
}
|
|
6018
|
+
appendEvent({
|
|
6019
|
+
type: 'lane_assign',
|
|
6020
|
+
id,
|
|
6021
|
+
ts: new Date().toISOString(),
|
|
6022
|
+
lane: name,
|
|
6023
|
+
});
|
|
6024
|
+
printLine(`${id} assigned to lane ${name}`);
|
|
6025
|
+
return { ...publicOutcome(record), lane: name };
|
|
6026
|
+
}
|
|
6027
|
+
|
|
5273
6028
|
const commands = {
|
|
5274
6029
|
begin(argv) {
|
|
5275
6030
|
const { positionals, flags } = parseArgs(argv, {
|
|
@@ -5324,6 +6079,7 @@ const commands = {
|
|
|
5324
6079
|
}
|
|
5325
6080
|
|
|
5326
6081
|
const id = nextId(events);
|
|
6082
|
+
const currentLane = currentLaneOrFail(records);
|
|
5327
6083
|
events.push(appendEvent({
|
|
5328
6084
|
type: 'begin',
|
|
5329
6085
|
id,
|
|
@@ -5332,6 +6088,7 @@ const commands = {
|
|
|
5332
6088
|
acceptance,
|
|
5333
6089
|
verify: flags.verify || null,
|
|
5334
6090
|
decisions,
|
|
6091
|
+
...(currentLane !== DEFAULT_LANE ? { lane: currentLane } : {}),
|
|
5335
6092
|
head: gitCapture(['rev-parse', 'HEAD']),
|
|
5336
6093
|
}));
|
|
5337
6094
|
const record = fold(events).find((candidate) => candidate.id === id);
|
|
@@ -5513,34 +6270,71 @@ const commands = {
|
|
|
5513
6270
|
printLine('parked v1 intent; close it with: driftseal end --status abandoned --note "close parked v1 intent before migration"');
|
|
5514
6271
|
return publicOutcome(parkedV1);
|
|
5515
6272
|
}
|
|
5516
|
-
const
|
|
6273
|
+
const view = loadOutcomeView({ repairTail: true, readOnly });
|
|
6274
|
+
const records = view.records;
|
|
6275
|
+
const { current, missing } = resolveCurrentLane(records);
|
|
6276
|
+
warnMissingCurrentLane(missing);
|
|
6277
|
+
const customLanes = records.lanes && records.lanes.size > 1;
|
|
6278
|
+
if (customLanes || current !== DEFAULT_LANE) {
|
|
6279
|
+
printLine(renderLaneLine(records, current));
|
|
6280
|
+
}
|
|
6281
|
+
const open = openOutcome(records);
|
|
5517
6282
|
if (!open) {
|
|
5518
6283
|
printLine('no outcome in progress');
|
|
5519
6284
|
return null;
|
|
5520
6285
|
}
|
|
5521
|
-
printLine(render(open));
|
|
6286
|
+
printLine(render(open, { currentLane: current }));
|
|
5522
6287
|
return publicOutcome(open);
|
|
5523
6288
|
},
|
|
5524
6289
|
|
|
5525
6290
|
log(argv, { readOnly = false } = {}) {
|
|
5526
|
-
const { positionals, flags } = parseArgs(argv, {
|
|
6291
|
+
const { positionals, flags } = parseArgs(argv, {
|
|
6292
|
+
last: '-n',
|
|
6293
|
+
all: 'boolean',
|
|
6294
|
+
'all-lanes': 'boolean',
|
|
6295
|
+
}, 'log');
|
|
5527
6296
|
if (positionals.length > 0) fail(usageFor('log'));
|
|
5528
|
-
|
|
6297
|
+
const view = loadOutcomeView({ repairTail: true, readOnly });
|
|
6298
|
+
let records = view.records;
|
|
5529
6299
|
const parkedV1 = legacyParkedIntent();
|
|
5530
6300
|
if (parkedV1 && !records.some((record) => record.id === parkedV1.id && record.status === 'in_progress')) {
|
|
5531
|
-
records = [...records, parkedV1];
|
|
6301
|
+
records = Object.assign([...records, parkedV1], { lanes: records.lanes });
|
|
6302
|
+
}
|
|
6303
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
6304
|
+
const { current, missing } = resolveCurrentLane(view.records);
|
|
6305
|
+
warnMissingCurrentLane(missing);
|
|
6306
|
+
const allLanes = flags['all-lanes'] === true;
|
|
6307
|
+
if (!allLanes) {
|
|
6308
|
+
records = Object.assign(selectLaneLogRecords(records, current), { lanes: catalog });
|
|
6309
|
+
}
|
|
6310
|
+
const visible = flags.all ? records : records.filter((record) => !record.reclaimed);
|
|
6311
|
+
const customLanes = catalog.size > 1;
|
|
6312
|
+
if (!allLanes && (customLanes || current !== DEFAULT_LANE || missing)) {
|
|
6313
|
+
printLine(renderLaneLine(view.records, current));
|
|
5532
6314
|
}
|
|
5533
|
-
|
|
6315
|
+
let shown = visible;
|
|
5534
6316
|
if (flags.last) {
|
|
5535
6317
|
const n = positiveInteger(flags.last, '--last');
|
|
5536
|
-
|
|
6318
|
+
shown = selectLastLogRecords(visible, current, n);
|
|
5537
6319
|
}
|
|
5538
|
-
if (
|
|
6320
|
+
if (shown.length === 0) {
|
|
5539
6321
|
printLine('log is empty');
|
|
5540
6322
|
return [];
|
|
5541
6323
|
}
|
|
5542
|
-
printLine(
|
|
5543
|
-
return
|
|
6324
|
+
printLine(shown.map((record) => render(record, { currentLane: current })).join('\n\n'));
|
|
6325
|
+
return shown.map(publicOutcome);
|
|
6326
|
+
},
|
|
6327
|
+
|
|
6328
|
+
lane(argv, { readOnly = false } = {}) {
|
|
6329
|
+
const [subcommand, ...rest] = argv;
|
|
6330
|
+
if (subcommand === '--help' || subcommand === '-h') throw new HelpRequested('lane');
|
|
6331
|
+
if (!subcommand || subcommand === 'show') {
|
|
6332
|
+
return showLanes(rest, { readOnly });
|
|
6333
|
+
}
|
|
6334
|
+
if (subcommand === 'add') return addLane(rest);
|
|
6335
|
+
if (subcommand === 'switch') return switchLane(rest);
|
|
6336
|
+
if (subcommand === 'assign') return assignLane(rest);
|
|
6337
|
+
fail(usageFor('lane'));
|
|
5544
6338
|
},
|
|
5545
6339
|
|
|
5546
6340
|
reclaim(argv) {
|
|
@@ -5906,6 +6700,9 @@ const commands = {
|
|
|
5906
6700
|
knownManagedBlocks: [
|
|
5907
6701
|
...sourceLanguages.flatMap((source) => [
|
|
5908
6702
|
protocolEol(intentProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
6703
|
+
protocolEol(intentProtocolBlock(PROTOCOL_VERSION, source, true), eol),
|
|
6704
|
+
protocolEol(intentProtocolBlockV20(source), eol),
|
|
6705
|
+
protocolEol(intentProtocolBlockV20(source, true), eol),
|
|
5909
6706
|
protocolEol(v1IntentProtocolBlock(14, source), eol),
|
|
5910
6707
|
protocolEol(v1IntentProtocolBlock(14, source, true), eol),
|
|
5911
6708
|
protocolEol(previousIntentProtocolBlock(13, source), eol),
|
|
@@ -5937,6 +6734,9 @@ const commands = {
|
|
|
5937
6734
|
knownManagedBlocks: [
|
|
5938
6735
|
...sourceLanguages.flatMap((source) => [
|
|
5939
6736
|
protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
6737
|
+
protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, source, true), eol),
|
|
6738
|
+
protocolEol(decisionProtocolBlock('2.0', source), eol),
|
|
6739
|
+
protocolEol(decisionProtocolBlock('2.0', source, true), eol),
|
|
5940
6740
|
protocolEol(v1DecisionProtocolBlock(14, source), eol),
|
|
5941
6741
|
protocolEol(v1DecisionProtocolBlock(14, source, true), eol),
|
|
5942
6742
|
protocolEol(previousDecisionProtocolBlock(13, source), eol),
|
|
@@ -6016,7 +6816,13 @@ usage:
|
|
|
6016
6816
|
to the current contract and Git-visible workspace
|
|
6017
6817
|
driftseal end [id] [--status completed|partial|failed|abandoned] [--note "..."] [--verify-result "..."]
|
|
6018
6818
|
driftseal status show the outcome currently in progress
|
|
6019
|
-
driftseal log [--last N] [--all]
|
|
6819
|
+
driftseal log [--last N] [--all] [--all-lanes]
|
|
6820
|
+
show outcome history (current lane; --all-lanes is global)
|
|
6821
|
+
driftseal lane show named outcome lanes and the current lane
|
|
6822
|
+
driftseal lane add <name> [--desc "..."]
|
|
6823
|
+
driftseal lane switch <name>
|
|
6824
|
+
driftseal lane assign <id> <name>
|
|
6825
|
+
partition outcome history by long-lived capability
|
|
6020
6826
|
driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]
|
|
6021
6827
|
hide meaningless closed records without deleting them
|
|
6022
6828
|
driftseal unreclaim <id> --reason "<why>"
|
|
@@ -6096,6 +6902,8 @@ const VALUE_TAKING_FLAGS = {
|
|
|
6096
6902
|
extend: ['--accept', '--verify', '-v', '--decision'],
|
|
6097
6903
|
end: ['--status', '-s', '--note', '-n', '--verify-result', '-r'],
|
|
6098
6904
|
log: ['--last', '-n'],
|
|
6905
|
+
lane: ['--desc'],
|
|
6906
|
+
'lane add': ['--desc'],
|
|
6099
6907
|
reclaim: ['--reason', '-r', '--older-than'],
|
|
6100
6908
|
unreclaim: ['--reason', '-r'],
|
|
6101
6909
|
absorb: ['--decisions'],
|
|
@@ -6133,6 +6941,7 @@ function mutationResources(cmd, argv) {
|
|
|
6133
6941
|
if (cmd === 'init') return [process.cwd()];
|
|
6134
6942
|
if (cmd === 'migrate') return [process.cwd()];
|
|
6135
6943
|
if (cmd === 'reclaim' || cmd === 'unreclaim') return [logDir()];
|
|
6944
|
+
if (cmd === 'lane') return [logDir()];
|
|
6136
6945
|
if (cmd === 'absorb' && argv[0] === '--git') {
|
|
6137
6946
|
const ours = argv[2];
|
|
6138
6947
|
return ours ? [path.dirname(path.resolve(ours))] : [process.cwd()];
|
|
@@ -6152,7 +6961,7 @@ function mutationResources(cmd, argv) {
|
|
|
6152
6961
|
|
|
6153
6962
|
function usesV2RepositoryState(cmd, rest) {
|
|
6154
6963
|
if (
|
|
6155
|
-
['begin', 'extend', 'verify', 'end', 'status', 'log', 'reclaim', 'unreclaim', 'absorb', 'decision', 'init'].includes(cmd)
|
|
6964
|
+
['begin', 'extend', 'verify', 'end', 'status', 'log', 'lane', 'reclaim', 'unreclaim', 'absorb', 'decision', 'init'].includes(cmd)
|
|
6156
6965
|
) {
|
|
6157
6966
|
return true;
|
|
6158
6967
|
}
|
|
@@ -6421,11 +7230,14 @@ function dispatch(argv) {
|
|
|
6421
7230
|
assertV2RepositoryReady(cmd, rest);
|
|
6422
7231
|
const mutates =
|
|
6423
7232
|
['begin', 'extend', 'end', 'init', 'skill', 'mcp', 'reclaim', 'unreclaim', 'absorb'].includes(cmd) ||
|
|
7233
|
+
(cmd === 'lane' && ['add', 'switch', 'assign'].includes(rest[0])) ||
|
|
6424
7234
|
(cmd === 'migrate' && rest[1] === 'apply') ||
|
|
6425
7235
|
(cmd === 'hook' && rest[0] === 'install') ||
|
|
6426
7236
|
(cmd === 'decision' && ['add', 'update'].includes(rest[0]));
|
|
6427
7237
|
const readsIntentLog =
|
|
6428
|
-
['status', 'log'].includes(cmd) ||
|
|
7238
|
+
['status', 'log'].includes(cmd) ||
|
|
7239
|
+
(cmd === 'lane' && (!rest[0] || rest[0] === 'show')) ||
|
|
7240
|
+
(cmd === 'hook' && ['prompt', 'stop'].includes(rest[0]));
|
|
6429
7241
|
if (mutates || readsIntentLog) {
|
|
6430
7242
|
if (readsIntentLog) {
|
|
6431
7243
|
let resources;
|
|
@@ -6595,12 +7407,27 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
|
6595
7407
|
appendFlag(argv, '--verify-result', verifyResult);
|
|
6596
7408
|
return call(argv);
|
|
6597
7409
|
},
|
|
6598
|
-
log({ last, all = false } = {}) {
|
|
7410
|
+
log({ last, all = false, allLanes = false } = {}) {
|
|
6599
7411
|
const argv = ['log'];
|
|
6600
7412
|
appendFlag(argv, '--last', last);
|
|
6601
7413
|
if (all) argv.push('--all');
|
|
7414
|
+
if (allLanes) argv.push('--all-lanes');
|
|
6602
7415
|
return call(argv);
|
|
6603
7416
|
},
|
|
7417
|
+
lane() {
|
|
7418
|
+
return call(['lane']);
|
|
7419
|
+
},
|
|
7420
|
+
laneAdd({ name, description } = {}) {
|
|
7421
|
+
const argv = ['lane', 'add', String(name)];
|
|
7422
|
+
appendFlag(argv, '--desc', description);
|
|
7423
|
+
return call(argv);
|
|
7424
|
+
},
|
|
7425
|
+
laneSwitch({ name } = {}) {
|
|
7426
|
+
return call(['lane', 'switch', String(name)]);
|
|
7427
|
+
},
|
|
7428
|
+
laneAssign({ id, lane } = {}) {
|
|
7429
|
+
return call(['lane', 'assign', String(id), String(lane)]);
|
|
7430
|
+
},
|
|
6604
7431
|
absorb({ otherLog, otherDecisions, abandon, dryRun = false } = {}) {
|
|
6605
7432
|
if (abandon && !['ours', 'theirs'].includes(abandon)) {
|
|
6606
7433
|
fail('absorb abandon must be "ours" or "theirs"');
|