driftseal 0.2.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 +39 -1
- package/README.zh-CN.md +39 -1
- package/bin/driftseal-mcp.js +335 -0
- package/bin/driftseal.js +250 -44
- package/package.json +7 -2
- package/skills/use-driftseal/SKILL.md +13 -5
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:
|
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
|
修改文件前,先声明这轮工作的目标:
|
|
@@ -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
|
@@ -40,6 +40,42 @@ 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 -->';
|
|
@@ -1232,19 +1294,21 @@ const commands = {
|
|
|
1232
1294
|
'superseded by --force',
|
|
1233
1295
|
null
|
|
1234
1296
|
);
|
|
1235
|
-
|
|
1297
|
+
printError(`driftseal: ${status} ${open.id}`);
|
|
1236
1298
|
}
|
|
1237
1299
|
|
|
1238
1300
|
const id = nextId(events);
|
|
1239
|
-
appendEvent({
|
|
1301
|
+
events.push(appendEvent({
|
|
1240
1302
|
type: 'begin',
|
|
1241
1303
|
id,
|
|
1242
1304
|
ts: new Date().toISOString(),
|
|
1243
1305
|
intent,
|
|
1244
1306
|
verify: flags.verify || null,
|
|
1245
1307
|
decisions,
|
|
1246
|
-
});
|
|
1247
|
-
|
|
1308
|
+
}));
|
|
1309
|
+
const record = fold(events).find((candidate) => candidate.id === id);
|
|
1310
|
+
printLine(id);
|
|
1311
|
+
return publicIntent(record);
|
|
1248
1312
|
},
|
|
1249
1313
|
|
|
1250
1314
|
end(argv) {
|
|
@@ -1279,8 +1343,9 @@ const commands = {
|
|
|
1279
1343
|
flags.note,
|
|
1280
1344
|
flags['verify-result']
|
|
1281
1345
|
);
|
|
1282
|
-
|
|
1283
|
-
|
|
1346
|
+
const record = fold(events).find((candidate) => candidate.id === target.id);
|
|
1347
|
+
printLine(`${target.id} ${terminalStatus}`);
|
|
1348
|
+
return publicIntent(record);
|
|
1284
1349
|
}
|
|
1285
1350
|
|
|
1286
1351
|
if (['completed', 'partial'].includes(status) && target.decisions.length > 0) {
|
|
@@ -1317,15 +1382,17 @@ const commands = {
|
|
|
1317
1382
|
}
|
|
1318
1383
|
}
|
|
1319
1384
|
|
|
1320
|
-
appendEvent({
|
|
1385
|
+
events.push(appendEvent({
|
|
1321
1386
|
type: 'end',
|
|
1322
1387
|
id: target.id,
|
|
1323
1388
|
ts: new Date().toISOString(),
|
|
1324
1389
|
status,
|
|
1325
1390
|
note: flags.note || null,
|
|
1326
1391
|
verifyResult: flags['verify-result'] || null,
|
|
1327
|
-
});
|
|
1328
|
-
|
|
1392
|
+
}));
|
|
1393
|
+
const record = fold(events).find((candidate) => candidate.id === target.id);
|
|
1394
|
+
printLine(`${target.id} ${status}`);
|
|
1395
|
+
return publicIntent(record);
|
|
1329
1396
|
},
|
|
1330
1397
|
|
|
1331
1398
|
status(argv) {
|
|
@@ -1333,10 +1400,11 @@ const commands = {
|
|
|
1333
1400
|
if (positionals.length > 0) fail('usage: driftseal status');
|
|
1334
1401
|
const open = openIntent(fold(readEvents({ repairTail: true })));
|
|
1335
1402
|
if (!open) {
|
|
1336
|
-
|
|
1337
|
-
return;
|
|
1403
|
+
printLine('no intent in progress');
|
|
1404
|
+
return null;
|
|
1338
1405
|
}
|
|
1339
|
-
|
|
1406
|
+
printLine(render(open));
|
|
1407
|
+
return publicIntent(open);
|
|
1340
1408
|
},
|
|
1341
1409
|
|
|
1342
1410
|
log(argv) {
|
|
@@ -1348,10 +1416,11 @@ const commands = {
|
|
|
1348
1416
|
records = records.slice(-n);
|
|
1349
1417
|
}
|
|
1350
1418
|
if (records.length === 0) {
|
|
1351
|
-
|
|
1352
|
-
return;
|
|
1419
|
+
printLine('log is empty');
|
|
1420
|
+
return [];
|
|
1353
1421
|
}
|
|
1354
|
-
|
|
1422
|
+
printLine(records.map(render).join('\n\n'));
|
|
1423
|
+
return records.map(publicIntent);
|
|
1355
1424
|
},
|
|
1356
1425
|
|
|
1357
1426
|
decision(argv) {
|
|
@@ -1392,8 +1461,9 @@ const commands = {
|
|
|
1392
1461
|
});
|
|
1393
1462
|
ensureDirectoryDurable(decisionDir());
|
|
1394
1463
|
atomicCreateFile(path.join(decisionDir(), file), content);
|
|
1395
|
-
|
|
1396
|
-
|
|
1464
|
+
const decision = findDecision(String(id));
|
|
1465
|
+
printLine(path.join(decisionDir(), file));
|
|
1466
|
+
return publicDecision(decision, { includeContent: true });
|
|
1397
1467
|
}
|
|
1398
1468
|
|
|
1399
1469
|
if (subcommand === 'update') {
|
|
@@ -1431,8 +1501,9 @@ const commands = {
|
|
|
1431
1501
|
fail('simulated interruption after decision write');
|
|
1432
1502
|
}
|
|
1433
1503
|
appendEvent(reconciliationEvent('decision_reconcile_commit', update));
|
|
1434
|
-
|
|
1435
|
-
|
|
1504
|
+
const reconciled = findDecision(decision.id);
|
|
1505
|
+
printLine(`${decision.id} ${update.fromStatus} -> ${update.toStatus} (${intent.id})`);
|
|
1506
|
+
return publicDecision(reconciled, { includeContent: true });
|
|
1436
1507
|
}
|
|
1437
1508
|
|
|
1438
1509
|
if (subcommand === 'list') {
|
|
@@ -1448,8 +1519,8 @@ const commands = {
|
|
|
1448
1519
|
}
|
|
1449
1520
|
const index = decisionIndex();
|
|
1450
1521
|
if (flags.count && !status) {
|
|
1451
|
-
|
|
1452
|
-
return;
|
|
1522
|
+
printLine(index.length);
|
|
1523
|
+
return { count: index.length };
|
|
1453
1524
|
}
|
|
1454
1525
|
let records = decisionCatalog(!status && last ? index.slice(-last) : index);
|
|
1455
1526
|
if (status) {
|
|
@@ -1457,19 +1528,19 @@ const commands = {
|
|
|
1457
1528
|
}
|
|
1458
1529
|
if (status && last) records = records.slice(-last);
|
|
1459
1530
|
if (flags.count) {
|
|
1460
|
-
|
|
1461
|
-
return;
|
|
1531
|
+
printLine(records.length);
|
|
1532
|
+
return { count: records.length };
|
|
1462
1533
|
}
|
|
1463
1534
|
if (records.length === 0) {
|
|
1464
|
-
|
|
1465
|
-
return;
|
|
1535
|
+
printLine(status ? `no decision records with status ${status}` : 'decision log is empty');
|
|
1536
|
+
return [];
|
|
1466
1537
|
}
|
|
1467
|
-
|
|
1538
|
+
printLine(
|
|
1468
1539
|
records
|
|
1469
1540
|
.map((record) => `[${record.id}] ${titleCase(record.status)} — ${record.title}\n ${record.file}`)
|
|
1470
1541
|
.join('\n')
|
|
1471
1542
|
);
|
|
1472
|
-
return;
|
|
1543
|
+
return records.map(publicDecision);
|
|
1473
1544
|
}
|
|
1474
1545
|
|
|
1475
1546
|
if (subcommand === 'show') {
|
|
@@ -1478,8 +1549,8 @@ const commands = {
|
|
|
1478
1549
|
fail('usage: driftseal decision show <id>');
|
|
1479
1550
|
}
|
|
1480
1551
|
const decision = findDecision(positionals[0]);
|
|
1481
|
-
|
|
1482
|
-
return;
|
|
1552
|
+
writeOutput(decision.content);
|
|
1553
|
+
return publicDecision(decision, { includeContent: true });
|
|
1483
1554
|
}
|
|
1484
1555
|
|
|
1485
1556
|
fail('usage: driftseal decision add|update|list|show (run: driftseal help)');
|
|
@@ -1539,15 +1610,16 @@ const commands = {
|
|
|
1539
1610
|
}
|
|
1540
1611
|
|
|
1541
1612
|
if (updated === current) {
|
|
1542
|
-
|
|
1543
|
-
return;
|
|
1613
|
+
printLine('AGENTS.md already contains the DriftSeal protocols; nothing to do');
|
|
1614
|
+
return { changed: false, target };
|
|
1544
1615
|
}
|
|
1545
1616
|
atomicWriteFile(target, updated);
|
|
1546
|
-
|
|
1617
|
+
printLine(`DriftSeal protocol ${existed ? 'updated in' : 'written to'} ${target}`);
|
|
1618
|
+
return { changed: true, target };
|
|
1547
1619
|
},
|
|
1548
1620
|
|
|
1549
1621
|
help() {
|
|
1550
|
-
|
|
1622
|
+
printLine(`DriftSeal — Seal the intent. Stop the drift.
|
|
1551
1623
|
|
|
1552
1624
|
Intent-level write-ahead log for agent sessions.
|
|
1553
1625
|
|
|
@@ -1573,6 +1645,7 @@ decision add options:
|
|
|
1573
1645
|
|
|
1574
1646
|
intent log: $DRIFTSEAL_HOME/events.jsonl, or .intent-log/events.jsonl
|
|
1575
1647
|
decision log: $DRIFTSEAL_DECISION_HOME, or .decision-log/ in the current directory`);
|
|
1648
|
+
return null;
|
|
1576
1649
|
},
|
|
1577
1650
|
};
|
|
1578
1651
|
|
|
@@ -1595,11 +1668,10 @@ function mutationResources(cmd, argv) {
|
|
|
1595
1668
|
return [logDir(), decisionDir()];
|
|
1596
1669
|
}
|
|
1597
1670
|
|
|
1598
|
-
function
|
|
1599
|
-
const [cmd, ...rest] =
|
|
1671
|
+
function dispatch(argv) {
|
|
1672
|
+
const [cmd, ...rest] = argv;
|
|
1600
1673
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
1601
|
-
commands.help();
|
|
1602
|
-
process.exit(cmd ? 0 : 1);
|
|
1674
|
+
return { data: commands.help(), exitCode: cmd ? 0 : 1 };
|
|
1603
1675
|
}
|
|
1604
1676
|
const fn = commands[cmd];
|
|
1605
1677
|
if (!fn) fail(`unknown command: ${cmd} (run: driftseal help)`);
|
|
@@ -1609,10 +1681,144 @@ function main() {
|
|
|
1609
1681
|
const readsIntentLog = ['status', 'log'].includes(cmd);
|
|
1610
1682
|
if (mutates || readsIntentLog) {
|
|
1611
1683
|
const resources = readsIntentLog ? [logDir()] : mutationResources(cmd, rest);
|
|
1612
|
-
withMutationLocks(resources, () => fn(rest));
|
|
1613
|
-
} else {
|
|
1614
|
-
fn(rest);
|
|
1684
|
+
return { data: withMutationLocks(resources, () => fn(rest)), exitCode: 0 };
|
|
1615
1685
|
}
|
|
1686
|
+
return { data: fn(rest), exitCode: 0 };
|
|
1616
1687
|
}
|
|
1617
1688
|
|
|
1618
|
-
|
|
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;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
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
|
|