driftseal 0.1.0 → 0.3.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 +40 -2
- package/README.zh-CN.md +40 -2
- package/bin/driftseal-mcp.js +335 -0
- package/bin/driftseal.js +271 -48
- package/package.json +7 -2
- package/skills/use-driftseal/SKILL.md +18 -8
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Before an agent touches the code, DriftSeal records what this round will accompl
|
|
|
12
12
|
seal intent → do the work → prove the result → close the round
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
**One open intent. One declared proof. One durable trail.** No service
|
|
15
|
+
**One open intent. One declared proof. One durable trail.** No service and no database—just local Node.js tools and plain files that travel with the repo.
|
|
16
16
|
|
|
17
17
|
## The problem is not speed. It is drift.
|
|
18
18
|
|
|
@@ -46,6 +46,44 @@ npm link
|
|
|
46
46
|
|
|
47
47
|
The package includes `skills/use-driftseal`, an agent-agnostic companion skill that drives repository work through the complete DriftSeal loop while keeping decision records selective. Install or link it using your agent runtime’s skill discovery convention, then invoke `use-driftseal` by name.
|
|
48
48
|
|
|
49
|
+
## Use DriftSeal through MCP
|
|
50
|
+
|
|
51
|
+
The same package includes `driftseal-mcp`, a local stdio MCP server. It exposes
|
|
52
|
+
structured tools for the complete intent and decision workflow while reusing the
|
|
53
|
+
same locking, WAL, atomic-write, schema, and recovery implementation as the CLI.
|
|
54
|
+
The server never shells out to `driftseal` and does not parse CLI output.
|
|
55
|
+
|
|
56
|
+
Fix the server to one repository when starting it:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
driftseal-mcp --root /absolute/path/to/repository
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For Codex, add the installed command as a stdio MCP server:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
codex mcp add driftseal -- driftseal-mcp --root /absolute/path/to/repository
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The root is startup configuration, not a tool input. In MCP mode DriftSeal also
|
|
69
|
+
ignores inherited `DRIFTSEAL_HOME` and `DRIFTSEAL_DECISION_HOME` overrides, so a
|
|
70
|
+
tool call cannot redirect writes outside the selected repository.
|
|
71
|
+
|
|
72
|
+
The v1 server provides:
|
|
73
|
+
|
|
74
|
+
| MCP capability | Purpose |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `driftseal_status`, `driftseal_log` | Read the current intent and intent history. |
|
|
77
|
+
| `driftseal_begin`, `driftseal_end` | Open and honestly close a work round. |
|
|
78
|
+
| `driftseal_decision_list`, `driftseal_decision_show` | Find and read MADR records. |
|
|
79
|
+
| `driftseal_decision_add`, `driftseal_decision_update` | Add selective decisions and reconcile linked ones. |
|
|
80
|
+
| `driftseal://intent/current` | Read the current intent as a JSON resource. |
|
|
81
|
+
| `driftseal://intents/recent` | Read the ten most recent intents as a JSON resource. |
|
|
82
|
+
| `driftseal://decisions` | Read the decision catalog as a JSON resource. |
|
|
83
|
+
|
|
84
|
+
The companion skill remains important: MCP supplies controlled, structured
|
|
85
|
+
operations; the skill teaches the agent when to use them and how to avoid drift.
|
|
86
|
+
|
|
49
87
|
## A work round
|
|
50
88
|
|
|
51
89
|
Declare the round before changing files:
|
|
@@ -66,7 +104,7 @@ driftseal end \
|
|
|
66
104
|
|
|
67
105
|
If the scope changes, close the current intent as `partial` or `abandoned`, then start a new one. After context loss, use `driftseal status` and `driftseal log --last 3` to re-anchor.
|
|
68
106
|
|
|
69
|
-
|
|
107
|
+
Single-step commands that only build, check, or record work already done — compiling, running tests, `git add`/`git commit` — need no intent of their own. When a commit is authorized, staging and committing only the verified changes and the just-closed intent log finalizes that round. Any content change made while preparing the commit starts a new round.
|
|
70
108
|
|
|
71
109
|
## Commands
|
|
72
110
|
|
package/README.zh-CN.md
CHANGED
|
@@ -12,7 +12,7 @@ Agentic coding 可以很快。**DriftSeal 让这种快不以失控为代价。**
|
|
|
12
12
|
封存 intent → 执行工作 → 证明结果 → 关闭本轮
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
**一个 open intent,一份预先声明的验证标准,一条可靠留存的工作轨迹。** 不需要 service
|
|
15
|
+
**一个 open intent,一份预先声明的验证标准,一条可靠留存的工作轨迹。** 不需要 service,也不需要 database;只有本地 Node.js tools,以及跟着 repo 一起走的普通文件。
|
|
16
16
|
|
|
17
17
|
## 真正麻烦的不是慢,而是偏航
|
|
18
18
|
|
|
@@ -46,6 +46,44 @@ npm link
|
|
|
46
46
|
|
|
47
47
|
npm package 内含 `skills/use-driftseal`。这是一个不绑定特定 agent runtime 的配套 skill,会按完整 DriftSeal 闭环执行仓库任务,同时克制地使用 decision record。按照所用 agent runtime 的 skill discovery 约定安装或 link,之后通过名称 `use-driftseal` 调用即可。
|
|
48
48
|
|
|
49
|
+
## 通过 MCP 使用 DriftSeal
|
|
50
|
+
|
|
51
|
+
同一个 package 还提供本地 stdio MCP server:`driftseal-mcp`。它为完整的
|
|
52
|
+
intent 与 decision 工作流提供结构化 tools,并与 CLI 复用同一套锁、WAL、
|
|
53
|
+
atomic write、schema 和 recovery 实现。server 不会启动 `driftseal` 子进程,
|
|
54
|
+
也不需要解析 CLI 输出。
|
|
55
|
+
|
|
56
|
+
启动时把 server 固定到一个 repository:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
driftseal-mcp --root /absolute/path/to/repository
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
在 Codex 中,可以把安装后的命令添加为 stdio MCP server:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
codex mcp add driftseal -- driftseal-mcp --root /absolute/path/to/repository
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
root 只能在启动时配置,不是 tool input。MCP 模式也会忽略继承到进程中的
|
|
69
|
+
`DRIFTSEAL_HOME` 和 `DRIFTSEAL_DECISION_HOME` override,因此 tool call 不能把
|
|
70
|
+
写入重定向到所选 repository 之外。
|
|
71
|
+
|
|
72
|
+
v1 server 提供:
|
|
73
|
+
|
|
74
|
+
| MCP capability | 用途 |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `driftseal_status`, `driftseal_log` | 读取当前 intent 和 intent 历史。 |
|
|
77
|
+
| `driftseal_begin`, `driftseal_end` | 开启并诚实关闭一轮工作。 |
|
|
78
|
+
| `driftseal_decision_list`, `driftseal_decision_show` | 查找并读取 MADR record。 |
|
|
79
|
+
| `driftseal_decision_add`, `driftseal_decision_update` | 克制地增加 decision,并 reconcile 已关联的 decision。 |
|
|
80
|
+
| `driftseal://intent/current` | 以 JSON resource 读取当前 intent。 |
|
|
81
|
+
| `driftseal://intents/recent` | 以 JSON resource 读取最近十条 intent。 |
|
|
82
|
+
| `driftseal://decisions` | 以 JSON resource 读取 decision catalog。 |
|
|
83
|
+
|
|
84
|
+
配套 skill 仍然不可替代:MCP 提供受控、结构化的操作,skill 则告诉 agent
|
|
85
|
+
何时使用这些操作,以及怎样避免 drift。
|
|
86
|
+
|
|
49
87
|
## 一轮标准工作流
|
|
50
88
|
|
|
51
89
|
修改文件前,先声明这轮工作的目标:
|
|
@@ -66,7 +104,7 @@ driftseal end \
|
|
|
66
104
|
|
|
67
105
|
如果范围发生变化,先把当前 intent 以 `partial` 或 `abandoned` 关闭,再开启新的 intent。发生 context loss 后,用 `driftseal status` 和 `driftseal log --last 3` 重新锚定当前目标。
|
|
68
106
|
|
|
69
|
-
|
|
107
|
+
只负责构建、检查或记录已完成工作的单步命令(编译、跑测试、`git add`/`git commit`)不需要单独开启 intent。如果用户已经授权创建 Git commit,只把已验证的改动和刚关闭的 intent log 进行 stage 和 commit,就属于这一轮的持久化收尾。准备 commit 时一旦需要修改内容,就必须开启新一轮。
|
|
70
108
|
|
|
71
109
|
## 命令速览
|
|
72
110
|
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { createApi, DECISION_STATUSES, END_STATUSES } = require('./driftseal.js');
|
|
6
|
+
|
|
7
|
+
const SERVER_NAME = 'driftseal';
|
|
8
|
+
const SERVER_VERSION = require('../package.json').version;
|
|
9
|
+
|
|
10
|
+
function parseArguments(argv) {
|
|
11
|
+
let root = process.cwd();
|
|
12
|
+
for (let index = 0; index < argv.length; index++) {
|
|
13
|
+
const argument = argv[index];
|
|
14
|
+
if (argument === '--help' || argument === '-h') return { help: true, root };
|
|
15
|
+
if (argument === '--root') {
|
|
16
|
+
const value = argv[++index];
|
|
17
|
+
if (!value || value.startsWith('-')) throw new Error('--root requires a directory');
|
|
18
|
+
root = value;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (argument.startsWith('--root=')) {
|
|
22
|
+
const value = argument.slice('--root='.length);
|
|
23
|
+
if (!value) throw new Error('--root requires a directory');
|
|
24
|
+
root = value;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`unknown argument: ${argument}`);
|
|
28
|
+
}
|
|
29
|
+
return { help: false, root };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function helpText() {
|
|
33
|
+
return `DriftSeal MCP server
|
|
34
|
+
|
|
35
|
+
usage:
|
|
36
|
+
driftseal-mcp [--root <repository>]
|
|
37
|
+
|
|
38
|
+
The server uses stdio transport and fixes all DriftSeal state to the selected
|
|
39
|
+
repository. Tool calls cannot select another root.`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function jsonText(value) {
|
|
43
|
+
return JSON.stringify(value, null, 2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function success(structuredContent, summary) {
|
|
47
|
+
return {
|
|
48
|
+
structuredContent,
|
|
49
|
+
content: [{ type: 'text', text: summary || jsonText(structuredContent) }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function failure(error) {
|
|
54
|
+
const message = error && error.message ? error.message : String(error);
|
|
55
|
+
return {
|
|
56
|
+
isError: true,
|
|
57
|
+
content: [{ type: 'text', text: `DriftSeal rejected the operation: ${message}` }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function guarded(action) {
|
|
62
|
+
try {
|
|
63
|
+
return action();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return failure(error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function registerTools(server, api, z) {
|
|
70
|
+
const intentRecord = z.object({
|
|
71
|
+
id: z.string(),
|
|
72
|
+
intent: z.string(),
|
|
73
|
+
verify: z.string().nullable(),
|
|
74
|
+
decisions: z.array(z.string()),
|
|
75
|
+
status: z.enum(END_STATUSES).or(z.literal('in_progress')),
|
|
76
|
+
note: z.string().nullable(),
|
|
77
|
+
verifyResult: z.string().nullable(),
|
|
78
|
+
beganAt: z.string(),
|
|
79
|
+
endedAt: z.string().nullable(),
|
|
80
|
+
});
|
|
81
|
+
const decisionRecord = z.object({
|
|
82
|
+
id: z.string(),
|
|
83
|
+
title: z.string(),
|
|
84
|
+
status: z.enum(DECISION_STATUSES),
|
|
85
|
+
file: z.string(),
|
|
86
|
+
});
|
|
87
|
+
const decisionWithContent = decisionRecord.extend({ content: z.string() });
|
|
88
|
+
const closedStatus = z.enum(END_STATUSES);
|
|
89
|
+
const decisionStatus = z.enum(DECISION_STATUSES);
|
|
90
|
+
const decisionId = z.string().regex(/^\d+$/, 'decision id must contain only digits');
|
|
91
|
+
const nonEmpty = z.string().trim().min(1);
|
|
92
|
+
const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
|
|
93
|
+
const localWrite = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
94
|
+
|
|
95
|
+
server.registerTool(
|
|
96
|
+
'driftseal_status',
|
|
97
|
+
{
|
|
98
|
+
title: 'Get current DriftSeal intent',
|
|
99
|
+
description:
|
|
100
|
+
'Inspect the one intent currently in progress before repository work or after context loss. Returns null when no intent is open.',
|
|
101
|
+
inputSchema: {},
|
|
102
|
+
outputSchema: { root: z.string(), intent: intentRecord.nullable() },
|
|
103
|
+
annotations: readOnly,
|
|
104
|
+
},
|
|
105
|
+
async () =>
|
|
106
|
+
guarded(() => {
|
|
107
|
+
const intent = api.status();
|
|
108
|
+
return success(
|
|
109
|
+
{ root: api.root, intent },
|
|
110
|
+
intent ? `Intent ${intent.id} is ${intent.status}.` : 'No DriftSeal intent is in progress.'
|
|
111
|
+
);
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
server.registerTool(
|
|
116
|
+
'driftseal_begin',
|
|
117
|
+
{
|
|
118
|
+
title: 'Begin a DriftSeal intent',
|
|
119
|
+
description:
|
|
120
|
+
'Open one focused work-round intent before making repository changes. Fails if another intent is already open; close it explicitly first.',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
intent: nonEmpty.describe('Outcome this work round will accomplish.'),
|
|
123
|
+
verify: nonEmpty.optional().describe('Exact command or outcome check that will prove completion.'),
|
|
124
|
+
decisions: z
|
|
125
|
+
.array(decisionId)
|
|
126
|
+
.default([])
|
|
127
|
+
.describe('Existing decision IDs this round may change or explicitly confirm.'),
|
|
128
|
+
},
|
|
129
|
+
outputSchema: { root: z.string(), intent: intentRecord },
|
|
130
|
+
annotations: localWrite,
|
|
131
|
+
},
|
|
132
|
+
async (input) =>
|
|
133
|
+
guarded(() => {
|
|
134
|
+
const intent = api.begin(input);
|
|
135
|
+
return success({ root: api.root, intent }, `Opened DriftSeal intent ${intent.id}.`);
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
server.registerTool(
|
|
140
|
+
'driftseal_end',
|
|
141
|
+
{
|
|
142
|
+
title: 'Close a DriftSeal intent',
|
|
143
|
+
description:
|
|
144
|
+
'Close the current work-round intent with an honest terminal status, note, and verification result. Linked decisions must be reconciled before completed or partial closure.',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
id: z.string().optional().describe('Intent ID; omit to close the current open intent.'),
|
|
147
|
+
status: closedStatus.default('completed'),
|
|
148
|
+
note: nonEmpty.optional().describe('What actually happened in the round.'),
|
|
149
|
+
verifyResult: nonEmpty.optional().describe('Concise, honest result of the declared verification.'),
|
|
150
|
+
},
|
|
151
|
+
outputSchema: { root: z.string(), intent: intentRecord },
|
|
152
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
153
|
+
},
|
|
154
|
+
async (input) =>
|
|
155
|
+
guarded(() => {
|
|
156
|
+
const intent = api.end(input);
|
|
157
|
+
return success({ root: api.root, intent }, `Closed DriftSeal intent ${intent.id} as ${intent.status}.`);
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
server.registerTool(
|
|
162
|
+
'driftseal_log',
|
|
163
|
+
{
|
|
164
|
+
title: 'List DriftSeal intent history',
|
|
165
|
+
description:
|
|
166
|
+
'Review recent or complete DriftSeal intent history to re-anchor work and understand prior outcomes.',
|
|
167
|
+
inputSchema: { last: z.number().int().positive().max(100).optional() },
|
|
168
|
+
outputSchema: { root: z.string(), intents: z.array(intentRecord) },
|
|
169
|
+
annotations: readOnly,
|
|
170
|
+
},
|
|
171
|
+
async (input) =>
|
|
172
|
+
guarded(() => {
|
|
173
|
+
const intents = api.log(input);
|
|
174
|
+
return success({ root: api.root, intents }, `Found ${intents.length} DriftSeal intent records.`);
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
server.registerTool(
|
|
179
|
+
'driftseal_decision_list',
|
|
180
|
+
{
|
|
181
|
+
title: 'List DriftSeal decisions',
|
|
182
|
+
description:
|
|
183
|
+
'Find MADR decision records, optionally filtered by current status. Use this before showing or linking a decision.',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
status: decisionStatus.optional(),
|
|
186
|
+
last: z.number().int().positive().max(100).optional(),
|
|
187
|
+
},
|
|
188
|
+
outputSchema: { root: z.string(), decisions: z.array(decisionRecord) },
|
|
189
|
+
annotations: readOnly,
|
|
190
|
+
},
|
|
191
|
+
async (input) =>
|
|
192
|
+
guarded(() => {
|
|
193
|
+
const decisions = api.decisionList(input);
|
|
194
|
+
return success({ root: api.root, decisions }, `Found ${decisions.length} DriftSeal decisions.`);
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
server.registerTool(
|
|
199
|
+
'driftseal_decision_show',
|
|
200
|
+
{
|
|
201
|
+
title: 'Show a DriftSeal decision',
|
|
202
|
+
description: 'Read one complete MADR decision record by stable numeric ID.',
|
|
203
|
+
inputSchema: { id: decisionId },
|
|
204
|
+
outputSchema: { root: z.string(), decision: decisionWithContent },
|
|
205
|
+
annotations: readOnly,
|
|
206
|
+
},
|
|
207
|
+
async ({ id }) =>
|
|
208
|
+
guarded(() => {
|
|
209
|
+
const decision = api.decisionShow({ id });
|
|
210
|
+
return success({ root: api.root, decision }, `Loaded DriftSeal decision ${decision.id}.`);
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
server.registerTool(
|
|
215
|
+
'driftseal_decision_add',
|
|
216
|
+
{
|
|
217
|
+
title: 'Add a DriftSeal decision',
|
|
218
|
+
description:
|
|
219
|
+
'Create a MADR record only for durable rationale, rejected paths, deferred choices, or costly-to-reverse decisions that Git and the intent log cannot recover.',
|
|
220
|
+
inputSchema: {
|
|
221
|
+
title: nonEmpty,
|
|
222
|
+
context: nonEmpty,
|
|
223
|
+
outcome: nonEmpty,
|
|
224
|
+
status: decisionStatus.default('accepted'),
|
|
225
|
+
drivers: z.array(nonEmpty).default([]),
|
|
226
|
+
options: z.array(nonEmpty).default([]),
|
|
227
|
+
consequences: z.array(nonEmpty).default([]),
|
|
228
|
+
},
|
|
229
|
+
outputSchema: { root: z.string(), decision: decisionWithContent },
|
|
230
|
+
annotations: localWrite,
|
|
231
|
+
},
|
|
232
|
+
async (input) =>
|
|
233
|
+
guarded(() => {
|
|
234
|
+
const decision = api.decisionAdd(input);
|
|
235
|
+
return success({ root: api.root, decision }, `Created DriftSeal decision ${decision.id}.`);
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
server.registerTool(
|
|
240
|
+
'driftseal_decision_update',
|
|
241
|
+
{
|
|
242
|
+
title: 'Reconcile a DriftSeal decision',
|
|
243
|
+
description:
|
|
244
|
+
'Reconcile one decision linked to the current open intent, updating or explicitly confirming its status with a history note.',
|
|
245
|
+
inputSchema: {
|
|
246
|
+
id: decisionId,
|
|
247
|
+
status: decisionStatus.optional(),
|
|
248
|
+
note: nonEmpty,
|
|
249
|
+
},
|
|
250
|
+
outputSchema: { root: z.string(), decision: decisionWithContent },
|
|
251
|
+
annotations: localWrite,
|
|
252
|
+
},
|
|
253
|
+
async (input) =>
|
|
254
|
+
guarded(() => {
|
|
255
|
+
const decision = api.decisionUpdate(input);
|
|
256
|
+
return success({ root: api.root, decision }, `Reconciled DriftSeal decision ${decision.id}.`);
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function registerResources(server, api) {
|
|
262
|
+
const registerJson = (name, uri, title, description, read) => {
|
|
263
|
+
server.registerResource(
|
|
264
|
+
name,
|
|
265
|
+
uri,
|
|
266
|
+
{ title, description, mimeType: 'application/json' },
|
|
267
|
+
async () => {
|
|
268
|
+
const value = read();
|
|
269
|
+
return { contents: [{ uri, mimeType: 'application/json', text: jsonText(value) }] };
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
registerJson(
|
|
275
|
+
'current-intent',
|
|
276
|
+
'driftseal://intent/current',
|
|
277
|
+
'Current DriftSeal intent',
|
|
278
|
+
'The work-round intent currently in progress for the fixed repository.',
|
|
279
|
+
() => ({ root: api.root, intent: api.status() })
|
|
280
|
+
);
|
|
281
|
+
registerJson(
|
|
282
|
+
'recent-intents',
|
|
283
|
+
'driftseal://intents/recent',
|
|
284
|
+
'Recent DriftSeal intents',
|
|
285
|
+
'The ten most recent work-round intent records for the fixed repository.',
|
|
286
|
+
() => ({ root: api.root, intents: api.log({ last: 10 }) })
|
|
287
|
+
);
|
|
288
|
+
registerJson(
|
|
289
|
+
'decision-catalog',
|
|
290
|
+
'driftseal://decisions',
|
|
291
|
+
'DriftSeal decision catalog',
|
|
292
|
+
'All MADR decision summaries for the fixed repository.',
|
|
293
|
+
() => ({ root: api.root, decisions: api.decisionList() })
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function createServer({ root }) {
|
|
298
|
+
const [{ McpServer }, { StdioServerTransport }, zod] = await Promise.all([
|
|
299
|
+
import('@modelcontextprotocol/sdk/server/mcp.js'),
|
|
300
|
+
import('@modelcontextprotocol/sdk/server/stdio.js'),
|
|
301
|
+
import('zod'),
|
|
302
|
+
]);
|
|
303
|
+
const z = zod.z || zod.default || zod;
|
|
304
|
+
const api = createApi({ root: path.resolve(root), isolateStorage: true });
|
|
305
|
+
const server = new McpServer(
|
|
306
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
307
|
+
{
|
|
308
|
+
instructions:
|
|
309
|
+
'Use driftseal_status before repository changes or after context loss. Open one focused intent with driftseal_begin before changes, then run the declared verification and close it honestly with driftseal_end. Reconcile every linked decision before completed or partial closure.',
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
registerTools(server, api, z);
|
|
313
|
+
registerResources(server, api);
|
|
314
|
+
return { server, transport: new StdioServerTransport(), root: api.root };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function main() {
|
|
318
|
+
const options = parseArguments(process.argv.slice(2));
|
|
319
|
+
if (options.help) {
|
|
320
|
+
process.stdout.write(helpText() + '\n');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const { server, transport } = await createServer(options);
|
|
324
|
+
await server.connect(transport);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = { createServer, helpText, parseArguments, registerResources, registerTools };
|
|
328
|
+
|
|
329
|
+
if (require.main === module) {
|
|
330
|
+
main().catch((error) => {
|
|
331
|
+
const message = error && error.message ? error.message : String(error);
|
|
332
|
+
console.error(`driftseal-mcp: error: ${message}`);
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
});
|
|
335
|
+
}
|
package/bin/driftseal.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Intent-level write-ahead log and MADR decision log for agentic coding sessions.
|
|
8
8
|
*
|
|
9
9
|
* Protocol per work round:
|
|
10
|
-
* 1. driftseal begin "<intent>" [--verify "<how to verify>"] (before
|
|
10
|
+
* 1. driftseal begin "<intent>" [--verify "<how to verify>"] (before changes that may need a rollback)
|
|
11
11
|
* 2. execute the intent
|
|
12
12
|
* 3. driftseal end [--status ...] [--note ...] [--verify-result ...] (reconcile against intent)
|
|
13
13
|
*
|
|
@@ -35,11 +35,47 @@ const DECISION_STATUSES = [
|
|
|
35
35
|
'superseded',
|
|
36
36
|
];
|
|
37
37
|
const EVENT_SCHEMA_VERSION = 2;
|
|
38
|
-
const PROTOCOL_VERSION =
|
|
38
|
+
const PROTOCOL_VERSION = 5;
|
|
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
|
}
|
|
@@ -176,7 +212,8 @@ function readEvents({ repairTail = false } = {}) {
|
|
|
176
212
|
.map((line, i) => {
|
|
177
213
|
try {
|
|
178
214
|
return normalizeEvent(JSON.parse(line), i + 1);
|
|
179
|
-
} catch {
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (err instanceof DriftSealError) throw err;
|
|
180
217
|
fail(`corrupt log line ${i + 1} in ${file}`);
|
|
181
218
|
}
|
|
182
219
|
});
|
|
@@ -928,8 +965,7 @@ function closeIntentAsEscape(events, record, requestedStatus, note, verifyResult
|
|
|
928
965
|
}
|
|
929
966
|
|
|
930
967
|
function fail(msg) {
|
|
931
|
-
|
|
932
|
-
process.exit(1);
|
|
968
|
+
throw new DriftSealError(msg);
|
|
933
969
|
}
|
|
934
970
|
|
|
935
971
|
function positiveInteger(value, flag) {
|
|
@@ -1005,6 +1041,32 @@ function render(rec) {
|
|
|
1005
1041
|
return lines.join('\n');
|
|
1006
1042
|
}
|
|
1007
1043
|
|
|
1044
|
+
function publicIntent(rec) {
|
|
1045
|
+
if (!rec) return null;
|
|
1046
|
+
return {
|
|
1047
|
+
id: rec.id,
|
|
1048
|
+
intent: rec.intent,
|
|
1049
|
+
verify: rec.verify,
|
|
1050
|
+
decisions: [...rec.decisions],
|
|
1051
|
+
status: rec.status,
|
|
1052
|
+
note: rec.note,
|
|
1053
|
+
verifyResult: rec.verifyResult,
|
|
1054
|
+
beganAt: rec.tsBegin,
|
|
1055
|
+
endedAt: rec.tsEnd,
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function publicDecision(decision, { includeContent = false } = {}) {
|
|
1060
|
+
const record = {
|
|
1061
|
+
id: decision.id,
|
|
1062
|
+
title: decision.title,
|
|
1063
|
+
status: decision.status,
|
|
1064
|
+
file: decision.file,
|
|
1065
|
+
};
|
|
1066
|
+
if (includeContent) record.content = decision.content;
|
|
1067
|
+
return record;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1008
1070
|
const INTENT_PROTOCOL_MARKER = '<!-- driftseal -->';
|
|
1009
1071
|
const INTENT_PROTOCOL_END = '<!-- /driftseal -->';
|
|
1010
1072
|
const DECISION_PROTOCOL_MARKER = '<!-- driftseal-decisions -->';
|
|
@@ -1018,9 +1080,12 @@ function intentProtocolBlock(version = PROTOCOL_VERSION) {
|
|
|
1018
1080
|
|
|
1019
1081
|
This repo uses DriftSeal (\`driftseal\`) to prevent agent drift. Every work round:
|
|
1020
1082
|
|
|
1021
|
-
1. **Write intent first**, before modifying
|
|
1083
|
+
1. **Write intent first**, before modifying, creating, or deleting files, or
|
|
1084
|
+
making any other change that may need a rollback:
|
|
1022
1085
|
\`driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"\`.
|
|
1023
1086
|
Add one \`--decision <id>\` for each existing decision this round may change.
|
|
1087
|
+
Single-step commands that only build, check, or record work already done
|
|
1088
|
+
(compiling, running tests, \`git add\`/\`git commit\`) need no intent.
|
|
1024
1089
|
2. **Execute only the intent.** Scope change? Close the current intent
|
|
1025
1090
|
(\`driftseal end -s partial|abandoned -n "<why>"\`) and \`driftseal begin\` a new one.
|
|
1026
1091
|
3. **Verify, then close**: run the declared verification, then
|
|
@@ -1044,7 +1109,19 @@ ${INTENT_PROTOCOL_END}`;
|
|
|
1044
1109
|
}
|
|
1045
1110
|
|
|
1046
1111
|
function previousIntentProtocolBlock(version) {
|
|
1047
|
-
|
|
1112
|
+
const v4 = intentProtocolBlock(version).replace(
|
|
1113
|
+
'1. **Write intent first**, before modifying, creating, or deleting files, or\n' +
|
|
1114
|
+
' making any other change that may need a rollback:\n' +
|
|
1115
|
+
' `driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"`.\n' +
|
|
1116
|
+
' Add one `--decision <id>` for each existing decision this round may change.\n' +
|
|
1117
|
+
' Single-step commands that only build, check, or record work already done\n' +
|
|
1118
|
+
' (compiling, running tests, `git add`/`git commit`) need no intent.',
|
|
1119
|
+
'1. **Write intent first**, before modifying a file or running a mutating command:\n' +
|
|
1120
|
+
' `driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"`.\n' +
|
|
1121
|
+
' Add one `--decision <id>` for each existing decision this round may change.'
|
|
1122
|
+
);
|
|
1123
|
+
if (version >= 4) return v4;
|
|
1124
|
+
return v4.replace(
|
|
1048
1125
|
' by the next linked `decision update` or successful `end`. Closing as\n' +
|
|
1049
1126
|
' `failed` or `abandoned` cancels pending recovery for that intent.',
|
|
1050
1127
|
' by the next `decision update` or `end`.'
|
|
@@ -1217,19 +1294,21 @@ const commands = {
|
|
|
1217
1294
|
'superseded by --force',
|
|
1218
1295
|
null
|
|
1219
1296
|
);
|
|
1220
|
-
|
|
1297
|
+
printError(`driftseal: ${status} ${open.id}`);
|
|
1221
1298
|
}
|
|
1222
1299
|
|
|
1223
1300
|
const id = nextId(events);
|
|
1224
|
-
appendEvent({
|
|
1301
|
+
events.push(appendEvent({
|
|
1225
1302
|
type: 'begin',
|
|
1226
1303
|
id,
|
|
1227
1304
|
ts: new Date().toISOString(),
|
|
1228
1305
|
intent,
|
|
1229
1306
|
verify: flags.verify || null,
|
|
1230
1307
|
decisions,
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1308
|
+
}));
|
|
1309
|
+
const record = fold(events).find((candidate) => candidate.id === id);
|
|
1310
|
+
printLine(id);
|
|
1311
|
+
return publicIntent(record);
|
|
1233
1312
|
},
|
|
1234
1313
|
|
|
1235
1314
|
end(argv) {
|
|
@@ -1264,8 +1343,9 @@ const commands = {
|
|
|
1264
1343
|
flags.note,
|
|
1265
1344
|
flags['verify-result']
|
|
1266
1345
|
);
|
|
1267
|
-
|
|
1268
|
-
|
|
1346
|
+
const record = fold(events).find((candidate) => candidate.id === target.id);
|
|
1347
|
+
printLine(`${target.id} ${terminalStatus}`);
|
|
1348
|
+
return publicIntent(record);
|
|
1269
1349
|
}
|
|
1270
1350
|
|
|
1271
1351
|
if (['completed', 'partial'].includes(status) && target.decisions.length > 0) {
|
|
@@ -1302,15 +1382,17 @@ const commands = {
|
|
|
1302
1382
|
}
|
|
1303
1383
|
}
|
|
1304
1384
|
|
|
1305
|
-
appendEvent({
|
|
1385
|
+
events.push(appendEvent({
|
|
1306
1386
|
type: 'end',
|
|
1307
1387
|
id: target.id,
|
|
1308
1388
|
ts: new Date().toISOString(),
|
|
1309
1389
|
status,
|
|
1310
1390
|
note: flags.note || null,
|
|
1311
1391
|
verifyResult: flags['verify-result'] || null,
|
|
1312
|
-
});
|
|
1313
|
-
|
|
1392
|
+
}));
|
|
1393
|
+
const record = fold(events).find((candidate) => candidate.id === target.id);
|
|
1394
|
+
printLine(`${target.id} ${status}`);
|
|
1395
|
+
return publicIntent(record);
|
|
1314
1396
|
},
|
|
1315
1397
|
|
|
1316
1398
|
status(argv) {
|
|
@@ -1318,10 +1400,11 @@ const commands = {
|
|
|
1318
1400
|
if (positionals.length > 0) fail('usage: driftseal status');
|
|
1319
1401
|
const open = openIntent(fold(readEvents({ repairTail: true })));
|
|
1320
1402
|
if (!open) {
|
|
1321
|
-
|
|
1322
|
-
return;
|
|
1403
|
+
printLine('no intent in progress');
|
|
1404
|
+
return null;
|
|
1323
1405
|
}
|
|
1324
|
-
|
|
1406
|
+
printLine(render(open));
|
|
1407
|
+
return publicIntent(open);
|
|
1325
1408
|
},
|
|
1326
1409
|
|
|
1327
1410
|
log(argv) {
|
|
@@ -1333,10 +1416,11 @@ const commands = {
|
|
|
1333
1416
|
records = records.slice(-n);
|
|
1334
1417
|
}
|
|
1335
1418
|
if (records.length === 0) {
|
|
1336
|
-
|
|
1337
|
-
return;
|
|
1419
|
+
printLine('log is empty');
|
|
1420
|
+
return [];
|
|
1338
1421
|
}
|
|
1339
|
-
|
|
1422
|
+
printLine(records.map(render).join('\n\n'));
|
|
1423
|
+
return records.map(publicIntent);
|
|
1340
1424
|
},
|
|
1341
1425
|
|
|
1342
1426
|
decision(argv) {
|
|
@@ -1377,8 +1461,9 @@ const commands = {
|
|
|
1377
1461
|
});
|
|
1378
1462
|
ensureDirectoryDurable(decisionDir());
|
|
1379
1463
|
atomicCreateFile(path.join(decisionDir(), file), content);
|
|
1380
|
-
|
|
1381
|
-
|
|
1464
|
+
const decision = findDecision(String(id));
|
|
1465
|
+
printLine(path.join(decisionDir(), file));
|
|
1466
|
+
return publicDecision(decision, { includeContent: true });
|
|
1382
1467
|
}
|
|
1383
1468
|
|
|
1384
1469
|
if (subcommand === 'update') {
|
|
@@ -1416,8 +1501,9 @@ const commands = {
|
|
|
1416
1501
|
fail('simulated interruption after decision write');
|
|
1417
1502
|
}
|
|
1418
1503
|
appendEvent(reconciliationEvent('decision_reconcile_commit', update));
|
|
1419
|
-
|
|
1420
|
-
|
|
1504
|
+
const reconciled = findDecision(decision.id);
|
|
1505
|
+
printLine(`${decision.id} ${update.fromStatus} -> ${update.toStatus} (${intent.id})`);
|
|
1506
|
+
return publicDecision(reconciled, { includeContent: true });
|
|
1421
1507
|
}
|
|
1422
1508
|
|
|
1423
1509
|
if (subcommand === 'list') {
|
|
@@ -1433,8 +1519,8 @@ const commands = {
|
|
|
1433
1519
|
}
|
|
1434
1520
|
const index = decisionIndex();
|
|
1435
1521
|
if (flags.count && !status) {
|
|
1436
|
-
|
|
1437
|
-
return;
|
|
1522
|
+
printLine(index.length);
|
|
1523
|
+
return { count: index.length };
|
|
1438
1524
|
}
|
|
1439
1525
|
let records = decisionCatalog(!status && last ? index.slice(-last) : index);
|
|
1440
1526
|
if (status) {
|
|
@@ -1442,19 +1528,19 @@ const commands = {
|
|
|
1442
1528
|
}
|
|
1443
1529
|
if (status && last) records = records.slice(-last);
|
|
1444
1530
|
if (flags.count) {
|
|
1445
|
-
|
|
1446
|
-
return;
|
|
1531
|
+
printLine(records.length);
|
|
1532
|
+
return { count: records.length };
|
|
1447
1533
|
}
|
|
1448
1534
|
if (records.length === 0) {
|
|
1449
|
-
|
|
1450
|
-
return;
|
|
1535
|
+
printLine(status ? `no decision records with status ${status}` : 'decision log is empty');
|
|
1536
|
+
return [];
|
|
1451
1537
|
}
|
|
1452
|
-
|
|
1538
|
+
printLine(
|
|
1453
1539
|
records
|
|
1454
1540
|
.map((record) => `[${record.id}] ${titleCase(record.status)} — ${record.title}\n ${record.file}`)
|
|
1455
1541
|
.join('\n')
|
|
1456
1542
|
);
|
|
1457
|
-
return;
|
|
1543
|
+
return records.map(publicDecision);
|
|
1458
1544
|
}
|
|
1459
1545
|
|
|
1460
1546
|
if (subcommand === 'show') {
|
|
@@ -1463,8 +1549,8 @@ const commands = {
|
|
|
1463
1549
|
fail('usage: driftseal decision show <id>');
|
|
1464
1550
|
}
|
|
1465
1551
|
const decision = findDecision(positionals[0]);
|
|
1466
|
-
|
|
1467
|
-
return;
|
|
1552
|
+
writeOutput(decision.content);
|
|
1553
|
+
return publicDecision(decision, { includeContent: true });
|
|
1468
1554
|
}
|
|
1469
1555
|
|
|
1470
1556
|
fail('usage: driftseal decision add|update|list|show (run: driftseal help)');
|
|
@@ -1489,6 +1575,7 @@ const commands = {
|
|
|
1489
1575
|
knownManagedBlocks: [
|
|
1490
1576
|
protocolEol(previousIntentProtocolBlock(2), eol),
|
|
1491
1577
|
protocolEol(previousIntentProtocolBlock(3), eol),
|
|
1578
|
+
protocolEol(previousIntentProtocolBlock(4), eol),
|
|
1492
1579
|
],
|
|
1493
1580
|
knownLegacyBlocks: [protocolEol(legacyIntentProtocolBlock(), eol)],
|
|
1494
1581
|
});
|
|
@@ -1502,6 +1589,7 @@ const commands = {
|
|
|
1502
1589
|
knownManagedBlocks: [
|
|
1503
1590
|
protocolEol(decisionProtocolBlock(2), eol),
|
|
1504
1591
|
protocolEol(decisionProtocolBlock(3), eol),
|
|
1592
|
+
protocolEol(decisionProtocolBlock(4), eol),
|
|
1505
1593
|
],
|
|
1506
1594
|
knownLegacyBlocks: [protocolEol(legacyDecisionProtocolBlock(), eol)],
|
|
1507
1595
|
});
|
|
@@ -1522,15 +1610,16 @@ const commands = {
|
|
|
1522
1610
|
}
|
|
1523
1611
|
|
|
1524
1612
|
if (updated === current) {
|
|
1525
|
-
|
|
1526
|
-
return;
|
|
1613
|
+
printLine('AGENTS.md already contains the DriftSeal protocols; nothing to do');
|
|
1614
|
+
return { changed: false, target };
|
|
1527
1615
|
}
|
|
1528
1616
|
atomicWriteFile(target, updated);
|
|
1529
|
-
|
|
1617
|
+
printLine(`DriftSeal protocol ${existed ? 'updated in' : 'written to'} ${target}`);
|
|
1618
|
+
return { changed: true, target };
|
|
1530
1619
|
},
|
|
1531
1620
|
|
|
1532
1621
|
help() {
|
|
1533
|
-
|
|
1622
|
+
printLine(`DriftSeal — Seal the intent. Stop the drift.
|
|
1534
1623
|
|
|
1535
1624
|
Intent-level write-ahead log for agent sessions.
|
|
1536
1625
|
|
|
@@ -1556,6 +1645,7 @@ decision add options:
|
|
|
1556
1645
|
|
|
1557
1646
|
intent log: $DRIFTSEAL_HOME/events.jsonl, or .intent-log/events.jsonl
|
|
1558
1647
|
decision log: $DRIFTSEAL_DECISION_HOME, or .decision-log/ in the current directory`);
|
|
1648
|
+
return null;
|
|
1559
1649
|
},
|
|
1560
1650
|
};
|
|
1561
1651
|
|
|
@@ -1578,11 +1668,10 @@ function mutationResources(cmd, argv) {
|
|
|
1578
1668
|
return [logDir(), decisionDir()];
|
|
1579
1669
|
}
|
|
1580
1670
|
|
|
1581
|
-
function
|
|
1582
|
-
const [cmd, ...rest] =
|
|
1671
|
+
function dispatch(argv) {
|
|
1672
|
+
const [cmd, ...rest] = argv;
|
|
1583
1673
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
1584
|
-
commands.help();
|
|
1585
|
-
process.exit(cmd ? 0 : 1);
|
|
1674
|
+
return { data: commands.help(), exitCode: cmd ? 0 : 1 };
|
|
1586
1675
|
}
|
|
1587
1676
|
const fn = commands[cmd];
|
|
1588
1677
|
if (!fn) fail(`unknown command: ${cmd} (run: driftseal help)`);
|
|
@@ -1592,10 +1681,144 @@ function main() {
|
|
|
1592
1681
|
const readsIntentLog = ['status', 'log'].includes(cmd);
|
|
1593
1682
|
if (mutates || readsIntentLog) {
|
|
1594
1683
|
const resources = readsIntentLog ? [logDir()] : mutationResources(cmd, rest);
|
|
1595
|
-
withMutationLocks(resources, () => fn(rest));
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1684
|
+
return { data: withMutationLocks(resources, () => fn(rest)), exitCode: 0 };
|
|
1685
|
+
}
|
|
1686
|
+
return { data: fn(rest), exitCode: 0 };
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function repositoryRoot(root) {
|
|
1690
|
+
if (typeof root !== 'string' || root.trim().length === 0) {
|
|
1691
|
+
fail('repository root must be a non-empty path');
|
|
1692
|
+
}
|
|
1693
|
+
const resolved = path.resolve(root);
|
|
1694
|
+
let stat;
|
|
1695
|
+
try {
|
|
1696
|
+
stat = fs.statSync(resolved);
|
|
1697
|
+
} catch {
|
|
1698
|
+
fail(`repository root does not exist: ${resolved}`);
|
|
1699
|
+
}
|
|
1700
|
+
if (!stat.isDirectory()) fail(`repository root is not a directory: ${resolved}`);
|
|
1701
|
+
return fs.realpathSync(resolved);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function runCommand(argv, { root = process.cwd(), isolateStorage = false, capture = true } = {}) {
|
|
1705
|
+
if (!Array.isArray(argv) || argv.some((arg) => typeof arg !== 'string')) {
|
|
1706
|
+
fail('command arguments must be an array of strings');
|
|
1707
|
+
}
|
|
1708
|
+
if (capture && activeOutput) fail('nested captured DriftSeal commands are not supported');
|
|
1709
|
+
|
|
1710
|
+
const fixedRoot = repositoryRoot(root);
|
|
1711
|
+
const previousCwd = process.cwd();
|
|
1712
|
+
const previousIntentHome = process.env.DRIFTSEAL_HOME;
|
|
1713
|
+
const previousDecisionHome = process.env.DRIFTSEAL_DECISION_HOME;
|
|
1714
|
+
const output = { stdout: '', stderr: '', data: null, exitCode: 0 };
|
|
1715
|
+
const previousOutput = activeOutput;
|
|
1716
|
+
|
|
1717
|
+
try {
|
|
1718
|
+
process.chdir(fixedRoot);
|
|
1719
|
+
if (isolateStorage) {
|
|
1720
|
+
delete process.env.DRIFTSEAL_HOME;
|
|
1721
|
+
delete process.env.DRIFTSEAL_DECISION_HOME;
|
|
1722
|
+
}
|
|
1723
|
+
if (capture) activeOutput = output;
|
|
1724
|
+
const result = dispatch(argv);
|
|
1725
|
+
output.data = result.data;
|
|
1726
|
+
output.exitCode = result.exitCode;
|
|
1727
|
+
return output;
|
|
1728
|
+
} catch (err) {
|
|
1729
|
+
if (capture) {
|
|
1730
|
+
err.stdout = output.stdout;
|
|
1731
|
+
err.stderr = output.stderr;
|
|
1732
|
+
}
|
|
1733
|
+
throw err;
|
|
1734
|
+
} finally {
|
|
1735
|
+
activeOutput = previousOutput;
|
|
1736
|
+
process.chdir(previousCwd);
|
|
1737
|
+
if (previousIntentHome === undefined) delete process.env.DRIFTSEAL_HOME;
|
|
1738
|
+
else process.env.DRIFTSEAL_HOME = previousIntentHome;
|
|
1739
|
+
if (previousDecisionHome === undefined) delete process.env.DRIFTSEAL_DECISION_HOME;
|
|
1740
|
+
else process.env.DRIFTSEAL_DECISION_HOME = previousDecisionHome;
|
|
1598
1741
|
}
|
|
1599
1742
|
}
|
|
1600
1743
|
|
|
1601
|
-
|
|
1744
|
+
function appendFlag(argv, flag, value) {
|
|
1745
|
+
if (value !== undefined && value !== null && value !== '') argv.push(flag, String(value));
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
1749
|
+
const fixedRoot = repositoryRoot(root);
|
|
1750
|
+
const call = (argv) => runCommand(argv, { root: fixedRoot, isolateStorage, capture: true }).data;
|
|
1751
|
+
return Object.freeze({
|
|
1752
|
+
root: fixedRoot,
|
|
1753
|
+
status() {
|
|
1754
|
+
return call(['status']);
|
|
1755
|
+
},
|
|
1756
|
+
begin({ intent, verify, decisions = [], force = false }) {
|
|
1757
|
+
const argv = ['begin', intent];
|
|
1758
|
+
appendFlag(argv, '--verify', verify);
|
|
1759
|
+
for (const decision of decisions) appendFlag(argv, '--decision', decision);
|
|
1760
|
+
if (force) argv.push('--force');
|
|
1761
|
+
return call(argv);
|
|
1762
|
+
},
|
|
1763
|
+
end({ id, status, note, verifyResult } = {}) {
|
|
1764
|
+
const argv = ['end'];
|
|
1765
|
+
if (id) argv.push(String(id));
|
|
1766
|
+
appendFlag(argv, '--status', status);
|
|
1767
|
+
appendFlag(argv, '--note', note);
|
|
1768
|
+
appendFlag(argv, '--verify-result', verifyResult);
|
|
1769
|
+
return call(argv);
|
|
1770
|
+
},
|
|
1771
|
+
log({ last } = {}) {
|
|
1772
|
+
const argv = ['log'];
|
|
1773
|
+
appendFlag(argv, '--last', last);
|
|
1774
|
+
return call(argv);
|
|
1775
|
+
},
|
|
1776
|
+
decisionAdd({ title, context, outcome, status, drivers = [], options = [], consequences = [] }) {
|
|
1777
|
+
const argv = ['decision', 'add', title, '--context', context, '--outcome', outcome];
|
|
1778
|
+
appendFlag(argv, '--status', status);
|
|
1779
|
+
for (const driver of drivers) appendFlag(argv, '--driver', driver);
|
|
1780
|
+
for (const option of options) appendFlag(argv, '--option', option);
|
|
1781
|
+
for (const consequence of consequences) appendFlag(argv, '--consequence', consequence);
|
|
1782
|
+
return call(argv);
|
|
1783
|
+
},
|
|
1784
|
+
decisionUpdate({ id, status, note }) {
|
|
1785
|
+
const argv = ['decision', 'update', String(id), '--note', note];
|
|
1786
|
+
appendFlag(argv, '--status', status);
|
|
1787
|
+
return call(argv);
|
|
1788
|
+
},
|
|
1789
|
+
decisionList({ status, last, count = false } = {}) {
|
|
1790
|
+
const argv = ['decision', 'list'];
|
|
1791
|
+
appendFlag(argv, '--status', status);
|
|
1792
|
+
appendFlag(argv, '--last', last);
|
|
1793
|
+
if (count) argv.push('--count');
|
|
1794
|
+
return call(argv);
|
|
1795
|
+
},
|
|
1796
|
+
decisionShow({ id }) {
|
|
1797
|
+
return call(['decision', 'show', String(id)]);
|
|
1798
|
+
},
|
|
1799
|
+
init() {
|
|
1800
|
+
return call(['init']);
|
|
1801
|
+
},
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function main() {
|
|
1806
|
+
try {
|
|
1807
|
+
const result = dispatch(process.argv.slice(2));
|
|
1808
|
+
process.exitCode = result.exitCode;
|
|
1809
|
+
} catch (err) {
|
|
1810
|
+
const message = err && err.message ? err.message : String(err);
|
|
1811
|
+
console.error(`driftseal: error: ${message}`);
|
|
1812
|
+
process.exitCode = 1;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
module.exports = {
|
|
1817
|
+
DECISION_STATUSES,
|
|
1818
|
+
END_STATUSES,
|
|
1819
|
+
DriftSealError,
|
|
1820
|
+
createApi,
|
|
1821
|
+
runCommand,
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
if (require.main === module) main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftseal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Seal intent, verification, and decisions into an auditable workflow for agentic coding",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"driftseal",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"type": "commonjs",
|
|
26
26
|
"main": "index.js",
|
|
27
27
|
"bin": {
|
|
28
|
-
"driftseal": "bin/driftseal.js"
|
|
28
|
+
"driftseal": "bin/driftseal.js",
|
|
29
|
+
"driftseal-mcp": "bin/driftseal-mcp.js"
|
|
29
30
|
},
|
|
30
31
|
"directories": {
|
|
31
32
|
"test": "test"
|
|
@@ -42,5 +43,9 @@
|
|
|
42
43
|
},
|
|
43
44
|
"engines": {
|
|
44
45
|
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
|
+
"zod": "^4.4.3"
|
|
45
50
|
}
|
|
46
51
|
}
|
|
@@ -11,15 +11,23 @@ activity streams.
|
|
|
11
11
|
|
|
12
12
|
## Locate DriftSeal
|
|
13
13
|
|
|
14
|
+
- Prefer the `driftseal_*` MCP tools when the DriftSeal MCP server is available
|
|
15
|
+
for the target repository. Use `driftseal_status`, `driftseal_begin`,
|
|
16
|
+
`driftseal_end`, and the corresponding decision tools instead of invoking the
|
|
17
|
+
CLI through a shell.
|
|
14
18
|
- Work from the repository root unless its instructions specify another scope.
|
|
15
|
-
-
|
|
16
|
-
`node bin/driftseal.js` when the global command is
|
|
17
|
-
|
|
19
|
+
- When MCP is unavailable, prefer `driftseal` from `PATH`. In a DriftSeal source
|
|
20
|
+
checkout, fall back to `node bin/driftseal.js` when the global command is
|
|
21
|
+
unavailable.
|
|
22
|
+
- Follow the repository's `AGENTS.md`. Storage overrides such as
|
|
23
|
+
`DRIFTSEAL_HOME` apply to CLI use; the MCP server deliberately keeps state in
|
|
24
|
+
its fixed repository root.
|
|
18
25
|
- If DriftSeal is unavailable, limit activity to read-only discovery and report the
|
|
19
26
|
blocker. Do not mutate the repository without the required log.
|
|
20
27
|
|
|
21
|
-
Use one
|
|
22
|
-
`driftseal`;
|
|
28
|
+
Use one interface consistently within a round. The examples below use
|
|
29
|
+
`driftseal`; map them directly to the same-named MCP workflow when MCP is
|
|
30
|
+
available, or substitute the local source command when necessary.
|
|
23
31
|
|
|
24
32
|
## Re-anchor Before Acting
|
|
25
33
|
|
|
@@ -35,8 +43,8 @@ open intent, stop mutating and coordinate instead of abandoning its work.
|
|
|
35
43
|
|
|
36
44
|
## Begin the Round
|
|
37
45
|
|
|
38
|
-
Before modifying
|
|
39
|
-
and its proof:
|
|
46
|
+
Before modifying, creating, or deleting files — or making any other change that
|
|
47
|
+
may need a rollback — declare one objective and its proof:
|
|
40
48
|
|
|
41
49
|
```sh
|
|
42
50
|
driftseal begin "<small objective for this round>" \
|
|
@@ -45,7 +53,9 @@ driftseal begin "<small objective for this round>" \
|
|
|
45
53
|
|
|
46
54
|
Make the intent small enough to finish and verify in one round. Prefer an
|
|
47
55
|
outcome-focused check over a vague activity such as "inspect the result."
|
|
48
|
-
Starting the intent is the first permitted mutation.
|
|
56
|
+
Starting the intent is the first permitted mutation. Single-step commands that
|
|
57
|
+
only build, check, or record work already done — compiling, running tests,
|
|
58
|
+
`git add`/`git commit` — need no intent of their own.
|
|
49
59
|
|
|
50
60
|
When the round may change or confirm an existing decision, declare each one at
|
|
51
61
|
the boundary with `--decision <id>`. Do not add decision links speculatively.
|