driftseal 0.2.0 → 0.4.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 -3
- package/README.zh-CN.md +57 -3
- package/bin/driftseal-mcp.js +405 -0
- package/bin/driftseal.js +445 -56
- package/package.json +10 -5
- package/skills/use-driftseal/SKILL.md +38 -130
package/bin/driftseal.js
CHANGED
|
@@ -34,12 +34,48 @@ const DECISION_STATUSES = [
|
|
|
34
34
|
'deprecated',
|
|
35
35
|
'superseded',
|
|
36
36
|
];
|
|
37
|
-
const EVENT_SCHEMA_VERSION =
|
|
38
|
-
const PROTOCOL_VERSION =
|
|
37
|
+
const EVENT_SCHEMA_VERSION = 3;
|
|
38
|
+
const PROTOCOL_VERSION = 7;
|
|
39
39
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
40
40
|
const LOCK_INIT_STALE_MS = 5 * 1000;
|
|
41
41
|
const MAX_DECISION_SLUG_LENGTH = 180;
|
|
42
42
|
|
|
43
|
+
class DriftSealError extends Error {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = 'DriftSealError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let activeOutput = null;
|
|
51
|
+
|
|
52
|
+
function printLine(value = '') {
|
|
53
|
+
const text = String(value);
|
|
54
|
+
if (activeOutput) {
|
|
55
|
+
activeOutput.stdout += text + '\n';
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
console.log(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function printError(value = '') {
|
|
62
|
+
const text = String(value);
|
|
63
|
+
if (activeOutput) {
|
|
64
|
+
activeOutput.stderr += text + '\n';
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.error(text);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeOutput(value) {
|
|
71
|
+
const text = String(value);
|
|
72
|
+
if (activeOutput) {
|
|
73
|
+
activeOutput.stdout += text;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write(text);
|
|
77
|
+
}
|
|
78
|
+
|
|
43
79
|
if (process.env._DRIFTSEAL_TEST_UMASK) {
|
|
44
80
|
process.umask(Number.parseInt(process.env._DRIFTSEAL_TEST_UMASK, 8));
|
|
45
81
|
}
|
|
@@ -94,6 +130,13 @@ function normalizeEvent(event, line) {
|
|
|
94
130
|
return event;
|
|
95
131
|
}
|
|
96
132
|
|
|
133
|
+
if (event.type === 'reclaim' || event.type === 'unreclaim') {
|
|
134
|
+
if (typeof event.reason !== 'string' || event.reason.trim().length === 0) {
|
|
135
|
+
fail(`invalid ${event.type} event on log line ${line}`);
|
|
136
|
+
}
|
|
137
|
+
return event;
|
|
138
|
+
}
|
|
139
|
+
|
|
97
140
|
if (
|
|
98
141
|
event.type === 'decision_reconcile' ||
|
|
99
142
|
event.type === 'decision_reconcile_prepare' ||
|
|
@@ -176,7 +219,8 @@ function readEvents({ repairTail = false } = {}) {
|
|
|
176
219
|
.map((line, i) => {
|
|
177
220
|
try {
|
|
178
221
|
return normalizeEvent(JSON.parse(line), i + 1);
|
|
179
|
-
} catch {
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (err instanceof DriftSealError) throw err;
|
|
180
224
|
fail(`corrupt log line ${i + 1} in ${file}`);
|
|
181
225
|
}
|
|
182
226
|
});
|
|
@@ -529,8 +573,28 @@ function fold(events) {
|
|
|
529
573
|
tsEnd: null,
|
|
530
574
|
note: null,
|
|
531
575
|
verifyResult: null,
|
|
576
|
+
reclaimed: false,
|
|
577
|
+
reclaimReason: null,
|
|
578
|
+
reclaimedAt: null,
|
|
532
579
|
});
|
|
533
580
|
order.push(ev.id);
|
|
581
|
+
} else if (ev.type === 'reclaim' || ev.type === 'unreclaim') {
|
|
582
|
+
const rec = records.get(ev.id);
|
|
583
|
+
if (!rec) fail(`${ev.type} event references unknown intent id: ${ev.id}`);
|
|
584
|
+
if (ev.type === 'reclaim') {
|
|
585
|
+
if (rec.status === 'in_progress') {
|
|
586
|
+
fail(`cannot reclaim intent ${ev.id} while it is in_progress`);
|
|
587
|
+
}
|
|
588
|
+
if (rec.reclaimed) fail(`duplicate reclaim event for intent id: ${ev.id}`);
|
|
589
|
+
rec.reclaimed = true;
|
|
590
|
+
rec.reclaimReason = ev.reason;
|
|
591
|
+
rec.reclaimedAt = ev.ts;
|
|
592
|
+
} else {
|
|
593
|
+
if (!rec.reclaimed) fail(`unreclaim event for intent id that is not reclaimed: ${ev.id}`);
|
|
594
|
+
rec.reclaimed = false;
|
|
595
|
+
rec.reclaimReason = null;
|
|
596
|
+
rec.reclaimedAt = null;
|
|
597
|
+
}
|
|
534
598
|
} else if (ev.type === 'end') {
|
|
535
599
|
const rec = records.get(ev.id);
|
|
536
600
|
if (!rec) fail(`end event references unknown intent id: ${ev.id}`);
|
|
@@ -928,8 +992,7 @@ function closeIntentAsEscape(events, record, requestedStatus, note, verifyResult
|
|
|
928
992
|
}
|
|
929
993
|
|
|
930
994
|
function fail(msg) {
|
|
931
|
-
|
|
932
|
-
process.exit(1);
|
|
995
|
+
throw new DriftSealError(msg);
|
|
933
996
|
}
|
|
934
997
|
|
|
935
998
|
function positiveInteger(value, flag) {
|
|
@@ -1002,9 +1065,39 @@ function render(rec) {
|
|
|
1002
1065
|
if (rec.verifyResult) lines.push(` verify-result: ${rec.verifyResult}`);
|
|
1003
1066
|
if (rec.note) lines.push(` note: ${rec.note}`);
|
|
1004
1067
|
lines.push(` began: ${rec.tsBegin}` + (rec.tsEnd ? ` ended: ${rec.tsEnd}` : ''));
|
|
1068
|
+
if (rec.reclaimed) lines.push(` reclaimed: ${rec.reclaimReason}`);
|
|
1005
1069
|
return lines.join('\n');
|
|
1006
1070
|
}
|
|
1007
1071
|
|
|
1072
|
+
function publicIntent(rec) {
|
|
1073
|
+
if (!rec) return null;
|
|
1074
|
+
return {
|
|
1075
|
+
id: rec.id,
|
|
1076
|
+
intent: rec.intent,
|
|
1077
|
+
verify: rec.verify,
|
|
1078
|
+
decisions: [...rec.decisions],
|
|
1079
|
+
status: rec.status,
|
|
1080
|
+
note: rec.note,
|
|
1081
|
+
verifyResult: rec.verifyResult,
|
|
1082
|
+
beganAt: rec.tsBegin,
|
|
1083
|
+
endedAt: rec.tsEnd,
|
|
1084
|
+
reclaimed: rec.reclaimed,
|
|
1085
|
+
reclaimReason: rec.reclaimReason,
|
|
1086
|
+
reclaimedAt: rec.reclaimedAt,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function publicDecision(decision, { includeContent = false } = {}) {
|
|
1091
|
+
const record = {
|
|
1092
|
+
id: decision.id,
|
|
1093
|
+
title: decision.title,
|
|
1094
|
+
status: decision.status,
|
|
1095
|
+
file: decision.file,
|
|
1096
|
+
};
|
|
1097
|
+
if (includeContent) record.content = decision.content;
|
|
1098
|
+
return record;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1008
1101
|
const INTENT_PROTOCOL_MARKER = '<!-- driftseal -->';
|
|
1009
1102
|
const INTENT_PROTOCOL_END = '<!-- /driftseal -->';
|
|
1010
1103
|
const DECISION_PROTOCOL_MARKER = '<!-- driftseal-decisions -->';
|
|
@@ -1040,14 +1133,38 @@ This repo uses DriftSeal (\`driftseal\`) to prevent agent drift. Every work roun
|
|
|
1040
1133
|
just-closed log finalizes that round without requiring a new intent. Any content
|
|
1041
1134
|
change made while preparing the commit does require a new intent.
|
|
1042
1135
|
4. **Re-anchor after context loss**: run \`driftseal status\` and \`driftseal log --last 3\` before
|
|
1043
|
-
doing anything else. The open intent is the source of truth
|
|
1136
|
+
doing anything else. The open intent is the source of truth: resume it when its
|
|
1137
|
+
objective still matches the current task; otherwise close it (\`partial\` or
|
|
1138
|
+
\`abandoned\`, with a note) and \`begin\` a new one.
|
|
1139
|
+
|
|
1140
|
+
**Log access goes only through DriftSeal.** Never read, edit, move, or delete
|
|
1141
|
+
\`.intent-log/events.jsonl\` (or anything under \`$DRIFTSEAL_HOME\`) directly; use
|
|
1142
|
+
\`driftseal\` commands or the MCP tools. Retire meaningless closed records with
|
|
1143
|
+
\`driftseal reclaim [id ...] --reason "<why>"\` — it appends a marker, never
|
|
1144
|
+
deletes log lines; \`driftseal unreclaim <id> --reason "<why>"\` restores one.
|
|
1044
1145
|
|
|
1045
1146
|
Log: \`.intent-log/events.jsonl\` (override with \`$DRIFTSEAL_HOME\`); commit it with the code.
|
|
1046
1147
|
${INTENT_PROTOCOL_END}`;
|
|
1047
1148
|
}
|
|
1048
1149
|
|
|
1049
1150
|
function previousIntentProtocolBlock(version) {
|
|
1050
|
-
const
|
|
1151
|
+
const v6 = intentProtocolBlock(version).replace(
|
|
1152
|
+
'doing anything else. The open intent is the source of truth: resume it when its\n' +
|
|
1153
|
+
' objective still matches the current task; otherwise close it (`partial` or\n' +
|
|
1154
|
+
' `abandoned`, with a note) and `begin` a new one.',
|
|
1155
|
+
'doing anything else. The open intent is the source of truth.'
|
|
1156
|
+
);
|
|
1157
|
+
if (version >= 6) return v6;
|
|
1158
|
+
const v5 = v6.replace(
|
|
1159
|
+
'\n**Log access goes only through DriftSeal.** Never read, edit, move, or delete\n' +
|
|
1160
|
+
'`.intent-log/events.jsonl` (or anything under `$DRIFTSEAL_HOME`) directly; use\n' +
|
|
1161
|
+
'`driftseal` commands or the MCP tools. Retire meaningless closed records with\n' +
|
|
1162
|
+
'`driftseal reclaim [id ...] --reason "<why>"` — it appends a marker, never\n' +
|
|
1163
|
+
'deletes log lines; `driftseal unreclaim <id> --reason "<why>"` restores one.\n',
|
|
1164
|
+
''
|
|
1165
|
+
);
|
|
1166
|
+
if (version >= 5) return v5;
|
|
1167
|
+
const v4 = v5.replace(
|
|
1051
1168
|
'1. **Write intent first**, before modifying, creating, or deleting files, or\n' +
|
|
1052
1169
|
' making any other change that may need a rollback:\n' +
|
|
1053
1170
|
' `driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"`.\n' +
|
|
@@ -1082,7 +1199,7 @@ revisiting, non-obvious rationale behind a long-lived or costly-to-reverse accep
|
|
|
1082
1199
|
choice, or a deprecated or superseded decision. Do not record routine, local,
|
|
1083
1200
|
readily reversible choices.
|
|
1084
1201
|
|
|
1085
|
-
\`driftseal decision add "<title>" --context "<problem and constraints>" --outcome "<decision and rationale>" --option "<considered option>" --consequence "<result>"\`
|
|
1202
|
+
\`driftseal decision add "<title>" --context "<problem and constraints>" --outcome "<decision and rationale>" --driver "<decision driver>" --option "<considered option>" --consequence "<result>"\`
|
|
1086
1203
|
|
|
1087
1204
|
Add one \`--driver\`, \`--option\`, or \`--consequence\` flag per item. Use
|
|
1088
1205
|
\`--status proposed|accepted|rejected|deferred|deprecated|superseded\` when needed.
|
|
@@ -1147,6 +1264,10 @@ When an intent declares an existing decision with \`--decision <id>\`, use
|
|
|
1147
1264
|
Commit \`.decision-log/\` with the code.`;
|
|
1148
1265
|
}
|
|
1149
1266
|
|
|
1267
|
+
function previousDecisionProtocolBlock(version) {
|
|
1268
|
+
return decisionProtocolBlock(version).replace(' --driver "<decision driver>"', '');
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1150
1271
|
function upgradeManagedBlock({
|
|
1151
1272
|
content,
|
|
1152
1273
|
marker,
|
|
@@ -1232,19 +1353,21 @@ const commands = {
|
|
|
1232
1353
|
'superseded by --force',
|
|
1233
1354
|
null
|
|
1234
1355
|
);
|
|
1235
|
-
|
|
1356
|
+
printError(`driftseal: ${status} ${open.id}`);
|
|
1236
1357
|
}
|
|
1237
1358
|
|
|
1238
1359
|
const id = nextId(events);
|
|
1239
|
-
appendEvent({
|
|
1360
|
+
events.push(appendEvent({
|
|
1240
1361
|
type: 'begin',
|
|
1241
1362
|
id,
|
|
1242
1363
|
ts: new Date().toISOString(),
|
|
1243
1364
|
intent,
|
|
1244
1365
|
verify: flags.verify || null,
|
|
1245
1366
|
decisions,
|
|
1246
|
-
});
|
|
1247
|
-
|
|
1367
|
+
}));
|
|
1368
|
+
const record = fold(events).find((candidate) => candidate.id === id);
|
|
1369
|
+
printLine(id);
|
|
1370
|
+
return publicIntent(record);
|
|
1248
1371
|
},
|
|
1249
1372
|
|
|
1250
1373
|
end(argv) {
|
|
@@ -1279,8 +1402,9 @@ const commands = {
|
|
|
1279
1402
|
flags.note,
|
|
1280
1403
|
flags['verify-result']
|
|
1281
1404
|
);
|
|
1282
|
-
|
|
1283
|
-
|
|
1405
|
+
const record = fold(events).find((candidate) => candidate.id === target.id);
|
|
1406
|
+
printLine(`${target.id} ${terminalStatus}`);
|
|
1407
|
+
return publicIntent(record);
|
|
1284
1408
|
}
|
|
1285
1409
|
|
|
1286
1410
|
if (['completed', 'partial'].includes(status) && target.decisions.length > 0) {
|
|
@@ -1317,15 +1441,17 @@ const commands = {
|
|
|
1317
1441
|
}
|
|
1318
1442
|
}
|
|
1319
1443
|
|
|
1320
|
-
appendEvent({
|
|
1444
|
+
events.push(appendEvent({
|
|
1321
1445
|
type: 'end',
|
|
1322
1446
|
id: target.id,
|
|
1323
1447
|
ts: new Date().toISOString(),
|
|
1324
1448
|
status,
|
|
1325
1449
|
note: flags.note || null,
|
|
1326
1450
|
verifyResult: flags['verify-result'] || null,
|
|
1327
|
-
});
|
|
1328
|
-
|
|
1451
|
+
}));
|
|
1452
|
+
const record = fold(events).find((candidate) => candidate.id === target.id);
|
|
1453
|
+
printLine(`${target.id} ${status}`);
|
|
1454
|
+
return publicIntent(record);
|
|
1329
1455
|
},
|
|
1330
1456
|
|
|
1331
1457
|
status(argv) {
|
|
@@ -1333,25 +1459,131 @@ const commands = {
|
|
|
1333
1459
|
if (positionals.length > 0) fail('usage: driftseal status');
|
|
1334
1460
|
const open = openIntent(fold(readEvents({ repairTail: true })));
|
|
1335
1461
|
if (!open) {
|
|
1336
|
-
|
|
1337
|
-
return;
|
|
1462
|
+
printLine('no intent in progress');
|
|
1463
|
+
return null;
|
|
1338
1464
|
}
|
|
1339
|
-
|
|
1465
|
+
printLine(render(open));
|
|
1466
|
+
return publicIntent(open);
|
|
1340
1467
|
},
|
|
1341
1468
|
|
|
1342
1469
|
log(argv) {
|
|
1343
|
-
const { positionals, flags } = parseArgs(argv, { last: '-n' });
|
|
1344
|
-
if (positionals.length > 0) fail('usage: driftseal log [--last N]');
|
|
1470
|
+
const { positionals, flags } = parseArgs(argv, { last: '-n', all: 'boolean' });
|
|
1471
|
+
if (positionals.length > 0) fail('usage: driftseal log [--last N] [--all]');
|
|
1345
1472
|
let records = fold(readEvents({ repairTail: true }));
|
|
1473
|
+
if (!flags.all) records = records.filter((record) => !record.reclaimed);
|
|
1346
1474
|
if (flags.last) {
|
|
1347
1475
|
const n = positiveInteger(flags.last, '--last');
|
|
1348
1476
|
records = records.slice(-n);
|
|
1349
1477
|
}
|
|
1350
1478
|
if (records.length === 0) {
|
|
1351
|
-
|
|
1352
|
-
return;
|
|
1479
|
+
printLine('log is empty');
|
|
1480
|
+
return [];
|
|
1353
1481
|
}
|
|
1354
|
-
|
|
1482
|
+
printLine(records.map(render).join('\n\n'));
|
|
1483
|
+
return records.map(publicIntent);
|
|
1484
|
+
},
|
|
1485
|
+
|
|
1486
|
+
reclaim(argv) {
|
|
1487
|
+
const { positionals, flags } = parseArgs(argv, {
|
|
1488
|
+
reason: '-r',
|
|
1489
|
+
'older-than': 'single',
|
|
1490
|
+
force: 'boolean',
|
|
1491
|
+
'dry-run': 'boolean',
|
|
1492
|
+
});
|
|
1493
|
+
const reason = flags.reason && flags.reason.trim();
|
|
1494
|
+
if (!reason) {
|
|
1495
|
+
fail(
|
|
1496
|
+
'usage: driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]'
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
let olderThanDays = 7;
|
|
1500
|
+
if (flags['older-than'] !== undefined) {
|
|
1501
|
+
olderThanDays = positiveInteger(flags['older-than'], '--older-than');
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const records = fold(readEvents({ repairTail: true }));
|
|
1505
|
+
let targets;
|
|
1506
|
+
if (positionals.length > 0) {
|
|
1507
|
+
const ids = [...new Set(positionals)];
|
|
1508
|
+
targets = ids.map((id) => {
|
|
1509
|
+
const record = records.find((candidate) => candidate.id === id);
|
|
1510
|
+
if (!record) fail(`unknown intent id: ${id}`);
|
|
1511
|
+
if (record.status === 'in_progress') {
|
|
1512
|
+
fail(`cannot reclaim intent ${id} while it is in_progress`);
|
|
1513
|
+
}
|
|
1514
|
+
if (record.reclaimed) fail(`intent ${id} is already reclaimed`);
|
|
1515
|
+
const routine = ['failed', 'abandoned'].includes(record.status) &&
|
|
1516
|
+
record.decisions.length === 0;
|
|
1517
|
+
if (!routine && !flags.force) {
|
|
1518
|
+
fail(
|
|
1519
|
+
`intent ${id} is ${record.status}` +
|
|
1520
|
+
(record.decisions.length > 0 ? ' and linked to decisions' : '') +
|
|
1521
|
+
'; re-run with --force to reclaim it anyway'
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
return record;
|
|
1525
|
+
});
|
|
1526
|
+
} else {
|
|
1527
|
+
if (flags.force) fail('--force requires explicit intent ids');
|
|
1528
|
+
const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
|
|
1529
|
+
targets = records.filter(
|
|
1530
|
+
(record) =>
|
|
1531
|
+
record.status !== 'in_progress' &&
|
|
1532
|
+
['failed', 'abandoned'].includes(record.status) &&
|
|
1533
|
+
record.decisions.length === 0 &&
|
|
1534
|
+
!record.reclaimed &&
|
|
1535
|
+
Date.parse(record.tsEnd) < cutoff
|
|
1536
|
+
);
|
|
1537
|
+
if (targets.length === 0) {
|
|
1538
|
+
printLine('no reclaimable intents');
|
|
1539
|
+
return [];
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
if (flags['dry-run']) {
|
|
1544
|
+
printLine(targets.map((record) => `${record.id} ${record.status} — ${record.intent}`).join('\n'));
|
|
1545
|
+
return targets.map(publicIntent);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
let events = readEvents({ repairTail: true });
|
|
1549
|
+
for (const record of targets) {
|
|
1550
|
+
events.push(
|
|
1551
|
+
appendEvent({
|
|
1552
|
+
type: 'reclaim',
|
|
1553
|
+
id: record.id,
|
|
1554
|
+
ts: new Date().toISOString(),
|
|
1555
|
+
reason,
|
|
1556
|
+
})
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
const reclaimed = fold(events).filter((record) =>
|
|
1560
|
+
targets.some((target) => target.id === record.id)
|
|
1561
|
+
);
|
|
1562
|
+
printLine(targets.map((record) => `${record.id} reclaimed`).join('\n'));
|
|
1563
|
+
return reclaimed.map(publicIntent);
|
|
1564
|
+
},
|
|
1565
|
+
|
|
1566
|
+
unreclaim(argv) {
|
|
1567
|
+
const { positionals, flags } = parseArgs(argv, { reason: '-r' });
|
|
1568
|
+
const reason = flags.reason && flags.reason.trim();
|
|
1569
|
+
if (positionals.length !== 1 || !reason) {
|
|
1570
|
+
fail('usage: driftseal unreclaim <id> --reason "<why>"');
|
|
1571
|
+
}
|
|
1572
|
+
const events = readEvents({ repairTail: true });
|
|
1573
|
+
const record = fold(events).find((candidate) => candidate.id === positionals[0]);
|
|
1574
|
+
if (!record) fail(`unknown intent id: ${positionals[0]}`);
|
|
1575
|
+
if (!record.reclaimed) fail(`intent ${positionals[0]} is not reclaimed`);
|
|
1576
|
+
events.push(
|
|
1577
|
+
appendEvent({
|
|
1578
|
+
type: 'unreclaim',
|
|
1579
|
+
id: record.id,
|
|
1580
|
+
ts: new Date().toISOString(),
|
|
1581
|
+
reason,
|
|
1582
|
+
})
|
|
1583
|
+
);
|
|
1584
|
+
const restored = fold(events).find((candidate) => candidate.id === record.id);
|
|
1585
|
+
printLine(`${record.id} unreclaimed`);
|
|
1586
|
+
return publicIntent(restored);
|
|
1355
1587
|
},
|
|
1356
1588
|
|
|
1357
1589
|
decision(argv) {
|
|
@@ -1392,8 +1624,9 @@ const commands = {
|
|
|
1392
1624
|
});
|
|
1393
1625
|
ensureDirectoryDurable(decisionDir());
|
|
1394
1626
|
atomicCreateFile(path.join(decisionDir(), file), content);
|
|
1395
|
-
|
|
1396
|
-
|
|
1627
|
+
const decision = findDecision(String(id));
|
|
1628
|
+
printLine(path.join(decisionDir(), file));
|
|
1629
|
+
return publicDecision(decision, { includeContent: true });
|
|
1397
1630
|
}
|
|
1398
1631
|
|
|
1399
1632
|
if (subcommand === 'update') {
|
|
@@ -1431,8 +1664,9 @@ const commands = {
|
|
|
1431
1664
|
fail('simulated interruption after decision write');
|
|
1432
1665
|
}
|
|
1433
1666
|
appendEvent(reconciliationEvent('decision_reconcile_commit', update));
|
|
1434
|
-
|
|
1435
|
-
|
|
1667
|
+
const reconciled = findDecision(decision.id);
|
|
1668
|
+
printLine(`${decision.id} ${update.fromStatus} -> ${update.toStatus} (${intent.id})`);
|
|
1669
|
+
return publicDecision(reconciled, { includeContent: true });
|
|
1436
1670
|
}
|
|
1437
1671
|
|
|
1438
1672
|
if (subcommand === 'list') {
|
|
@@ -1448,8 +1682,8 @@ const commands = {
|
|
|
1448
1682
|
}
|
|
1449
1683
|
const index = decisionIndex();
|
|
1450
1684
|
if (flags.count && !status) {
|
|
1451
|
-
|
|
1452
|
-
return;
|
|
1685
|
+
printLine(index.length);
|
|
1686
|
+
return { count: index.length };
|
|
1453
1687
|
}
|
|
1454
1688
|
let records = decisionCatalog(!status && last ? index.slice(-last) : index);
|
|
1455
1689
|
if (status) {
|
|
@@ -1457,19 +1691,19 @@ const commands = {
|
|
|
1457
1691
|
}
|
|
1458
1692
|
if (status && last) records = records.slice(-last);
|
|
1459
1693
|
if (flags.count) {
|
|
1460
|
-
|
|
1461
|
-
return;
|
|
1694
|
+
printLine(records.length);
|
|
1695
|
+
return { count: records.length };
|
|
1462
1696
|
}
|
|
1463
1697
|
if (records.length === 0) {
|
|
1464
|
-
|
|
1465
|
-
return;
|
|
1698
|
+
printLine(status ? `no decision records with status ${status}` : 'decision log is empty');
|
|
1699
|
+
return [];
|
|
1466
1700
|
}
|
|
1467
|
-
|
|
1701
|
+
printLine(
|
|
1468
1702
|
records
|
|
1469
1703
|
.map((record) => `[${record.id}] ${titleCase(record.status)} — ${record.title}\n ${record.file}`)
|
|
1470
1704
|
.join('\n')
|
|
1471
1705
|
);
|
|
1472
|
-
return;
|
|
1706
|
+
return records.map(publicDecision);
|
|
1473
1707
|
}
|
|
1474
1708
|
|
|
1475
1709
|
if (subcommand === 'show') {
|
|
@@ -1478,8 +1712,8 @@ const commands = {
|
|
|
1478
1712
|
fail('usage: driftseal decision show <id>');
|
|
1479
1713
|
}
|
|
1480
1714
|
const decision = findDecision(positionals[0]);
|
|
1481
|
-
|
|
1482
|
-
return;
|
|
1715
|
+
writeOutput(decision.content);
|
|
1716
|
+
return publicDecision(decision, { includeContent: true });
|
|
1483
1717
|
}
|
|
1484
1718
|
|
|
1485
1719
|
fail('usage: driftseal decision add|update|list|show (run: driftseal help)');
|
|
@@ -1505,6 +1739,8 @@ const commands = {
|
|
|
1505
1739
|
protocolEol(previousIntentProtocolBlock(2), eol),
|
|
1506
1740
|
protocolEol(previousIntentProtocolBlock(3), eol),
|
|
1507
1741
|
protocolEol(previousIntentProtocolBlock(4), eol),
|
|
1742
|
+
protocolEol(previousIntentProtocolBlock(5), eol),
|
|
1743
|
+
protocolEol(previousIntentProtocolBlock(6), eol),
|
|
1508
1744
|
],
|
|
1509
1745
|
knownLegacyBlocks: [protocolEol(legacyIntentProtocolBlock(), eol)],
|
|
1510
1746
|
});
|
|
@@ -1516,9 +1752,11 @@ const commands = {
|
|
|
1516
1752
|
versionPattern: /^<!-- driftseal-decisions-version: (\d+) -->\r?$/m,
|
|
1517
1753
|
replacement: decisionBlock,
|
|
1518
1754
|
knownManagedBlocks: [
|
|
1519
|
-
protocolEol(
|
|
1520
|
-
protocolEol(
|
|
1521
|
-
protocolEol(
|
|
1755
|
+
protocolEol(previousDecisionProtocolBlock(2), eol),
|
|
1756
|
+
protocolEol(previousDecisionProtocolBlock(3), eol),
|
|
1757
|
+
protocolEol(previousDecisionProtocolBlock(4), eol),
|
|
1758
|
+
protocolEol(previousDecisionProtocolBlock(5), eol),
|
|
1759
|
+
protocolEol(previousDecisionProtocolBlock(6), eol),
|
|
1522
1760
|
],
|
|
1523
1761
|
knownLegacyBlocks: [protocolEol(legacyDecisionProtocolBlock(), eol)],
|
|
1524
1762
|
});
|
|
@@ -1539,15 +1777,16 @@ const commands = {
|
|
|
1539
1777
|
}
|
|
1540
1778
|
|
|
1541
1779
|
if (updated === current) {
|
|
1542
|
-
|
|
1543
|
-
return;
|
|
1780
|
+
printLine('AGENTS.md already contains the DriftSeal protocols; nothing to do');
|
|
1781
|
+
return { changed: false, target };
|
|
1544
1782
|
}
|
|
1545
1783
|
atomicWriteFile(target, updated);
|
|
1546
|
-
|
|
1784
|
+
printLine(`DriftSeal protocol ${existed ? 'updated in' : 'written to'} ${target}`);
|
|
1785
|
+
return { changed: true, target };
|
|
1547
1786
|
},
|
|
1548
1787
|
|
|
1549
1788
|
help() {
|
|
1550
|
-
|
|
1789
|
+
printLine(`DriftSeal — Seal the intent. Stop the drift.
|
|
1551
1790
|
|
|
1552
1791
|
Intent-level write-ahead log for agent sessions.
|
|
1553
1792
|
|
|
@@ -1555,7 +1794,11 @@ usage:
|
|
|
1555
1794
|
driftseal begin "<intent>" [--verify "<how to verify>"] [--decision <id>] [--force]
|
|
1556
1795
|
driftseal end [id] [--status completed|partial|failed|abandoned] [--note "..."] [--verify-result "..."]
|
|
1557
1796
|
driftseal status show the intent currently in progress (re-anchor after drift)
|
|
1558
|
-
driftseal log [--last N]
|
|
1797
|
+
driftseal log [--last N] [--all] show intent history (--all includes reclaimed records)
|
|
1798
|
+
driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]
|
|
1799
|
+
hide meaningless closed records without deleting them
|
|
1800
|
+
driftseal unreclaim <id> --reason "<why>"
|
|
1801
|
+
restore a reclaimed record to the visible log
|
|
1559
1802
|
driftseal decision add "<title>" --context "..." --outcome "..." [options]
|
|
1560
1803
|
driftseal decision update <id> [--status STATUS] --note "..."
|
|
1561
1804
|
reconcile a linked decision in the open intent
|
|
@@ -1573,6 +1816,7 @@ decision add options:
|
|
|
1573
1816
|
|
|
1574
1817
|
intent log: $DRIFTSEAL_HOME/events.jsonl, or .intent-log/events.jsonl
|
|
1575
1818
|
decision log: $DRIFTSEAL_DECISION_HOME, or .decision-log/ in the current directory`);
|
|
1819
|
+
return null;
|
|
1576
1820
|
},
|
|
1577
1821
|
};
|
|
1578
1822
|
|
|
@@ -1586,6 +1830,7 @@ function requestedEndStatus(argv) {
|
|
|
1586
1830
|
|
|
1587
1831
|
function mutationResources(cmd, argv) {
|
|
1588
1832
|
if (cmd === 'init') return [process.cwd()];
|
|
1833
|
+
if (cmd === 'reclaim' || cmd === 'unreclaim') return [logDir()];
|
|
1589
1834
|
if (cmd === 'begin' && !argv.some((arg) => arg === '--decision' || arg.startsWith('--decision='))) {
|
|
1590
1835
|
return [logDir()];
|
|
1591
1836
|
}
|
|
@@ -1595,24 +1840,168 @@ function mutationResources(cmd, argv) {
|
|
|
1595
1840
|
return [logDir(), decisionDir()];
|
|
1596
1841
|
}
|
|
1597
1842
|
|
|
1598
|
-
function
|
|
1599
|
-
const [cmd, ...rest] =
|
|
1843
|
+
function dispatch(argv) {
|
|
1844
|
+
const [cmd, ...rest] = argv;
|
|
1600
1845
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
1601
|
-
commands.help();
|
|
1602
|
-
process.exit(cmd ? 0 : 1);
|
|
1846
|
+
return { data: commands.help(), exitCode: cmd ? 0 : 1 };
|
|
1603
1847
|
}
|
|
1604
1848
|
const fn = commands[cmd];
|
|
1605
1849
|
if (!fn) fail(`unknown command: ${cmd} (run: driftseal help)`);
|
|
1606
1850
|
const mutates =
|
|
1607
|
-
['begin', 'end', 'init'].includes(cmd) ||
|
|
1851
|
+
['begin', 'end', 'init', 'reclaim', 'unreclaim'].includes(cmd) ||
|
|
1608
1852
|
(cmd === 'decision' && ['add', 'update'].includes(rest[0]));
|
|
1609
1853
|
const readsIntentLog = ['status', 'log'].includes(cmd);
|
|
1610
1854
|
if (mutates || readsIntentLog) {
|
|
1611
1855
|
const resources = readsIntentLog ? [logDir()] : mutationResources(cmd, rest);
|
|
1612
|
-
withMutationLocks(resources, () => fn(rest));
|
|
1613
|
-
} else {
|
|
1614
|
-
fn(rest);
|
|
1856
|
+
return { data: withMutationLocks(resources, () => fn(rest)), exitCode: 0 };
|
|
1615
1857
|
}
|
|
1858
|
+
return { data: fn(rest), exitCode: 0 };
|
|
1616
1859
|
}
|
|
1617
1860
|
|
|
1618
|
-
|
|
1861
|
+
function repositoryRoot(root) {
|
|
1862
|
+
if (typeof root !== 'string' || root.trim().length === 0) {
|
|
1863
|
+
fail('repository root must be a non-empty path');
|
|
1864
|
+
}
|
|
1865
|
+
const resolved = path.resolve(root);
|
|
1866
|
+
let stat;
|
|
1867
|
+
try {
|
|
1868
|
+
stat = fs.statSync(resolved);
|
|
1869
|
+
} catch {
|
|
1870
|
+
fail(`repository root does not exist: ${resolved}`);
|
|
1871
|
+
}
|
|
1872
|
+
if (!stat.isDirectory()) fail(`repository root is not a directory: ${resolved}`);
|
|
1873
|
+
return fs.realpathSync(resolved);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function runCommand(argv, { root = process.cwd(), isolateStorage = false, capture = true } = {}) {
|
|
1877
|
+
if (!Array.isArray(argv) || argv.some((arg) => typeof arg !== 'string')) {
|
|
1878
|
+
fail('command arguments must be an array of strings');
|
|
1879
|
+
}
|
|
1880
|
+
if (capture && activeOutput) fail('nested captured DriftSeal commands are not supported');
|
|
1881
|
+
|
|
1882
|
+
const fixedRoot = repositoryRoot(root);
|
|
1883
|
+
const previousCwd = process.cwd();
|
|
1884
|
+
const previousIntentHome = process.env.DRIFTSEAL_HOME;
|
|
1885
|
+
const previousDecisionHome = process.env.DRIFTSEAL_DECISION_HOME;
|
|
1886
|
+
const output = { stdout: '', stderr: '', data: null, exitCode: 0 };
|
|
1887
|
+
const previousOutput = activeOutput;
|
|
1888
|
+
|
|
1889
|
+
try {
|
|
1890
|
+
process.chdir(fixedRoot);
|
|
1891
|
+
if (isolateStorage) {
|
|
1892
|
+
delete process.env.DRIFTSEAL_HOME;
|
|
1893
|
+
delete process.env.DRIFTSEAL_DECISION_HOME;
|
|
1894
|
+
}
|
|
1895
|
+
if (capture) activeOutput = output;
|
|
1896
|
+
const result = dispatch(argv);
|
|
1897
|
+
output.data = result.data;
|
|
1898
|
+
output.exitCode = result.exitCode;
|
|
1899
|
+
return output;
|
|
1900
|
+
} catch (err) {
|
|
1901
|
+
if (capture) {
|
|
1902
|
+
err.stdout = output.stdout;
|
|
1903
|
+
err.stderr = output.stderr;
|
|
1904
|
+
}
|
|
1905
|
+
throw err;
|
|
1906
|
+
} finally {
|
|
1907
|
+
activeOutput = previousOutput;
|
|
1908
|
+
process.chdir(previousCwd);
|
|
1909
|
+
if (previousIntentHome === undefined) delete process.env.DRIFTSEAL_HOME;
|
|
1910
|
+
else process.env.DRIFTSEAL_HOME = previousIntentHome;
|
|
1911
|
+
if (previousDecisionHome === undefined) delete process.env.DRIFTSEAL_DECISION_HOME;
|
|
1912
|
+
else process.env.DRIFTSEAL_DECISION_HOME = previousDecisionHome;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function appendFlag(argv, flag, value) {
|
|
1917
|
+
if (value !== undefined && value !== null && value !== '') argv.push(flag, String(value));
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
1921
|
+
const fixedRoot = repositoryRoot(root);
|
|
1922
|
+
const call = (argv) => runCommand(argv, { root: fixedRoot, isolateStorage, capture: true }).data;
|
|
1923
|
+
return Object.freeze({
|
|
1924
|
+
root: fixedRoot,
|
|
1925
|
+
status() {
|
|
1926
|
+
return call(['status']);
|
|
1927
|
+
},
|
|
1928
|
+
begin({ intent, verify, decisions = [], force = false }) {
|
|
1929
|
+
const argv = ['begin', intent];
|
|
1930
|
+
appendFlag(argv, '--verify', verify);
|
|
1931
|
+
for (const decision of decisions) appendFlag(argv, '--decision', decision);
|
|
1932
|
+
if (force) argv.push('--force');
|
|
1933
|
+
return call(argv);
|
|
1934
|
+
},
|
|
1935
|
+
end({ id, status, note, verifyResult } = {}) {
|
|
1936
|
+
const argv = ['end'];
|
|
1937
|
+
if (id) argv.push(String(id));
|
|
1938
|
+
appendFlag(argv, '--status', status);
|
|
1939
|
+
appendFlag(argv, '--note', note);
|
|
1940
|
+
appendFlag(argv, '--verify-result', verifyResult);
|
|
1941
|
+
return call(argv);
|
|
1942
|
+
},
|
|
1943
|
+
log({ last, all = false } = {}) {
|
|
1944
|
+
const argv = ['log'];
|
|
1945
|
+
appendFlag(argv, '--last', last);
|
|
1946
|
+
if (all) argv.push('--all');
|
|
1947
|
+
return call(argv);
|
|
1948
|
+
},
|
|
1949
|
+
reclaim({ ids = [], reason, olderThan, force = false, dryRun = false }) {
|
|
1950
|
+
const argv = ['reclaim', ...ids.map(String), '--reason', reason];
|
|
1951
|
+
appendFlag(argv, '--older-than', olderThan);
|
|
1952
|
+
if (force) argv.push('--force');
|
|
1953
|
+
if (dryRun) argv.push('--dry-run');
|
|
1954
|
+
return call(argv);
|
|
1955
|
+
},
|
|
1956
|
+
unreclaim({ id, reason }) {
|
|
1957
|
+
return call(['unreclaim', String(id), '--reason', reason]);
|
|
1958
|
+
},
|
|
1959
|
+
decisionAdd({ title, context, outcome, status, drivers = [], options = [], consequences = [] }) {
|
|
1960
|
+
const argv = ['decision', 'add', title, '--context', context, '--outcome', outcome];
|
|
1961
|
+
appendFlag(argv, '--status', status);
|
|
1962
|
+
for (const driver of drivers) appendFlag(argv, '--driver', driver);
|
|
1963
|
+
for (const option of options) appendFlag(argv, '--option', option);
|
|
1964
|
+
for (const consequence of consequences) appendFlag(argv, '--consequence', consequence);
|
|
1965
|
+
return call(argv);
|
|
1966
|
+
},
|
|
1967
|
+
decisionUpdate({ id, status, note }) {
|
|
1968
|
+
const argv = ['decision', 'update', String(id), '--note', note];
|
|
1969
|
+
appendFlag(argv, '--status', status);
|
|
1970
|
+
return call(argv);
|
|
1971
|
+
},
|
|
1972
|
+
decisionList({ status, last, count = false } = {}) {
|
|
1973
|
+
const argv = ['decision', 'list'];
|
|
1974
|
+
appendFlag(argv, '--status', status);
|
|
1975
|
+
appendFlag(argv, '--last', last);
|
|
1976
|
+
if (count) argv.push('--count');
|
|
1977
|
+
return call(argv);
|
|
1978
|
+
},
|
|
1979
|
+
decisionShow({ id }) {
|
|
1980
|
+
return call(['decision', 'show', String(id)]);
|
|
1981
|
+
},
|
|
1982
|
+
init() {
|
|
1983
|
+
return call(['init']);
|
|
1984
|
+
},
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function main() {
|
|
1989
|
+
try {
|
|
1990
|
+
const result = dispatch(process.argv.slice(2));
|
|
1991
|
+
process.exitCode = result.exitCode;
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
const message = err && err.message ? err.message : String(err);
|
|
1994
|
+
console.error(`driftseal: error: ${message}`);
|
|
1995
|
+
process.exitCode = 1;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
module.exports = {
|
|
2000
|
+
DECISION_STATUSES,
|
|
2001
|
+
END_STATUSES,
|
|
2002
|
+
DriftSealError,
|
|
2003
|
+
createApi,
|
|
2004
|
+
runCommand,
|
|
2005
|
+
};
|
|
2006
|
+
|
|
2007
|
+
if (require.main === module) main();
|