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 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, no database, no runtime dependencies—just a Node.js CLI and plain files that travel with the repo.
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,45 @@ 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_reclaim`, `driftseal_unreclaim` | Hide meaningless closed records behind append-only markers, or restore them. |
79
+ | `driftseal_decision_list`, `driftseal_decision_show` | Find and read MADR records. |
80
+ | `driftseal_decision_add`, `driftseal_decision_update` | Add selective decisions and reconcile linked ones. |
81
+ | `driftseal://intent/current` | Read the current intent as a JSON resource. |
82
+ | `driftseal://intents/recent` | Read the ten most recent intents as a JSON resource. |
83
+ | `driftseal://decisions` | Read the decision catalog as a JSON resource. |
84
+
85
+ The companion skill remains important: MCP supplies controlled, structured
86
+ operations; the skill teaches the agent when to use them and how to avoid drift.
87
+
49
88
  ## A work round
50
89
 
51
90
  Declare the round before changing files:
@@ -75,7 +114,9 @@ Single-step commands that only build, check, or record work already done — com
75
114
  | `driftseal begin "<intent>" [-v "<verify>"] [--decision id] [--force]` | Open a work-round intent and optionally link existing decisions. |
76
115
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | Close an intent honestly. |
77
116
  | `driftseal status` | Show the intent currently in progress. |
78
- | `driftseal log [-n N]` | Review intent history. |
117
+ | `driftseal log [-n N] [--all]` | Review intent history (`--all` includes reclaimed records). |
118
+ | `driftseal reclaim [id ...] --reason "..." [--older-than days] [--force] [--dry-run]` | Hide meaningless closed records behind append-only markers. |
119
+ | `driftseal unreclaim <id> --reason "..."` | Restore a reclaimed record to the visible log. |
79
120
  | `driftseal decision add "<title>" -c "..." -o "..."` | Write a numbered MADR decision. |
80
121
  | `driftseal decision update <id> [-s status] -n "..."` | Reconcile a linked decision in the open intent. |
81
122
  | `driftseal decision list [-s status] [--last N \| --count]` | List or count decision records, optionally filtered by status. |
@@ -89,6 +130,22 @@ close as `completed` or `partial`. The update changes the current status when
89
130
  requested and appends a timestamped history entry tied to the intent. Intents
90
131
  without decision links keep the ordinary workflow.
91
132
 
133
+ ## Reclaiming noise records
134
+
135
+ Some closed records stop mattering: a harness or sandbox failure is recorded
136
+ honestly as `failed`, but it says nothing about the project. `driftseal
137
+ reclaim` retires such records without rewriting history — it appends a
138
+ `reclaim` marker (with a mandatory `--reason`) to the same append-only log,
139
+ and reclaimed records disappear from `driftseal log` and `driftseal status`
140
+ output while remaining in `events.jsonl` and visible with `log --all`.
141
+ `driftseal unreclaim <id> --reason "..."` restores a record that turned out to
142
+ matter.
143
+
144
+ Without ids, batch mode reclaims only closed `failed`/`abandoned` records that
145
+ are not linked to decisions and are older than `--older-than` days (default
146
+ 7); use `--dry-run` to preview. `completed` and `partial` records, and any
147
+ decision-linked record, can only be reclaimed by explicit id with `--force`.
148
+
92
149
  ## Consistency and recovery
93
150
 
94
151
  DriftSeal serializes mutating commands with locks on the configured intent and
@@ -119,7 +176,7 @@ index.
119
176
 
120
177
  ## Storage
121
178
 
122
- - `.intent-log/events.jsonl` is the append-only intent log.
179
+ - `.intent-log/events.jsonl` is the append-only intent log. All access goes through `driftseal` (CLI or MCP) — never read, edit, move, or delete it directly; use `driftseal reclaim` to retire meaningless records instead of deleting log lines.
123
180
  - `.decision-log/` contains numbered MADR decision records.
124
181
  - Set `DRIFTSEAL_HOME` or `DRIFTSEAL_DECISION_HOME` to store either log outside the current project.
125
182
 
package/README.zh-CN.md CHANGED
@@ -12,7 +12,7 @@ Agentic coding 可以很快。**DriftSeal 让这种快不以失控为代价。**
12
12
  封存 intent → 执行工作 → 证明结果 → 关闭本轮
13
13
  ```
14
14
 
15
- **一个 open intent,一份预先声明的验证标准,一条可靠留存的工作轨迹。** 不需要 service,不需要 database,也没有 runtime dependencies;只有一个 Node.js CLI,以及跟着 repo 一起走的普通文件。
15
+ **一个 open intent,一份预先声明的验证标准,一条可靠留存的工作轨迹。** 不需要 service,也不需要 database;只有本地 Node.js tools,以及跟着 repo 一起走的普通文件。
16
16
 
17
17
  ## 真正麻烦的不是慢,而是偏航
18
18
 
@@ -46,6 +46,45 @@ 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_reclaim`, `driftseal_unreclaim` | 用 append-only 标记隐藏已无意义的已关闭记录,或将其恢复。 |
79
+ | `driftseal_decision_list`, `driftseal_decision_show` | 查找并读取 MADR record。 |
80
+ | `driftseal_decision_add`, `driftseal_decision_update` | 克制地增加 decision,并 reconcile 已关联的 decision。 |
81
+ | `driftseal://intent/current` | 以 JSON resource 读取当前 intent。 |
82
+ | `driftseal://intents/recent` | 以 JSON resource 读取最近十条 intent。 |
83
+ | `driftseal://decisions` | 以 JSON resource 读取 decision catalog。 |
84
+
85
+ 配套 skill 仍然不可替代:MCP 提供受控、结构化的操作,skill 则告诉 agent
86
+ 何时使用这些操作,以及怎样避免 drift。
87
+
49
88
  ## 一轮标准工作流
50
89
 
51
90
  修改文件前,先声明这轮工作的目标:
@@ -75,7 +114,9 @@ driftseal end \
75
114
  | `driftseal begin "<intent>" [-v "<verify>"] [--decision id] [--force]` | 开启一轮工作,并可关联已有 decision。 |
76
115
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | 诚实地关闭 intent。 |
77
116
  | `driftseal status` | 查看当前进行中的 intent。 |
78
- | `driftseal log [-n N]` | 查看 intent 历史。 |
117
+ | `driftseal log [-n N] [--all]` | 查看 intent 历史(`--all` 包含已回收的记录)。 |
118
+ | `driftseal reclaim [id ...] --reason "..." [--older-than days] [--force] [--dry-run]` | 用 append-only 标记隐藏已无意义的已关闭记录。 |
119
+ | `driftseal unreclaim <id> --reason "..."` | 把已回收的记录恢复到可见历史中。 |
79
120
  | `driftseal decision add "<title>" -c "..." -o "..."` | 写入编号化的 MADR decision。 |
80
121
  | `driftseal decision update <id> [-s status] -n "..."` | 在当前 intent 中 reconcile 已关联的 decision。 |
81
122
  | `driftseal decision list [-s status] [--last N \| --count]` | 列出或统计 decision records,也可按 status 筛选。 |
@@ -88,6 +129,19 @@ driftseal end \
88
129
  每一条关联 decision。update 可以改变当前 status,并会追加一条包含时间和
89
130
  intent ID 的 history。没有关联 decision 的 intent 仍沿用普通流程。
90
131
 
132
+ ## 回收已无意义的记录
133
+
134
+ 有些已关闭的记录会随着时间失去意义:harness 或 sandbox 导致的失败会被如实记录为
135
+ `failed`,但它与项目本身无关。`driftseal reclaim` 可以在不改写历史的前提下让这类
136
+ 记录退场——它只是向同一个 append-only log 追加一条 `reclaim` 标记(必须附带
137
+ `--reason`),被回收的记录会从 `driftseal log` 和 `driftseal status` 的输出中隐藏,
138
+ 但仍保留在 `events.jsonl` 中,并可通过 `log --all` 查看。若事后发现某条记录仍然
139
+ 重要,用 `driftseal unreclaim <id> --reason "..."` 恢复。
140
+
141
+ 不带 id 的批量模式只回收已关闭、未关联 decision、且早于 `--older-than` 天(默认 7
142
+ 天)的 `failed`/`abandoned` 记录;可以先用 `--dry-run` 预览。`completed` 和 `partial`
143
+ 记录,以及任何关联了 decision 的记录,只能按显式 id 加 `--force` 回收。
144
+
91
145
  ## 一致性与恢复
92
146
 
93
147
  DriftSeal 会对配置后的 intent log 与 decision log 根目录加锁,并按固定顺序获取这些
@@ -114,7 +168,7 @@ recovery 只处理当前 intent,因此历史冲突不会阻塞之后的 decisi
114
168
 
115
169
  ## 数据保存在哪里
116
170
 
117
- - `.intent-log/events.jsonl`:append-only intent log
171
+ - `.intent-log/events.jsonl`:append-only intent log。所有读写都必须经过 `driftseal`(CLI 或 MCP)——不要直接读取、修改、移动或删除该文件;需要让无意义的记录退场时使用 `driftseal reclaim`,而不是删除日志行。
118
172
  - `.decision-log/`:编号化的 MADR decision records。
119
173
  - 设置 `DRIFTSEAL_HOME` 或 `DRIFTSEAL_DECISION_HOME`,即可把对应 log 放到当前项目之外。
120
174
 
@@ -0,0 +1,405 @@
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
+ reclaimed: z.boolean(),
81
+ reclaimReason: z.string().nullable(),
82
+ reclaimedAt: z.string().nullable(),
83
+ });
84
+ const decisionRecord = z.object({
85
+ id: z.string(),
86
+ title: z.string(),
87
+ status: z.enum(DECISION_STATUSES),
88
+ file: z.string(),
89
+ });
90
+ const decisionWithContent = decisionRecord.extend({ content: z.string() });
91
+ const closedStatus = z.enum(END_STATUSES);
92
+ const decisionStatus = z.enum(DECISION_STATUSES);
93
+ const decisionId = z.string().regex(/^\d+$/, 'decision id must contain only digits');
94
+ const nonEmpty = z.string().trim().min(1);
95
+ const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
96
+ const localWrite = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
97
+
98
+ server.registerTool(
99
+ 'driftseal_status',
100
+ {
101
+ title: 'Get current DriftSeal intent',
102
+ description:
103
+ 'Inspect the one intent currently in progress before repository work or after context loss. Returns null when no intent is open.',
104
+ inputSchema: {},
105
+ outputSchema: { root: z.string(), intent: intentRecord.nullable() },
106
+ annotations: readOnly,
107
+ },
108
+ async () =>
109
+ guarded(() => {
110
+ const intent = api.status();
111
+ return success(
112
+ { root: api.root, intent },
113
+ intent ? `Intent ${intent.id} is ${intent.status}.` : 'No DriftSeal intent is in progress.'
114
+ );
115
+ })
116
+ );
117
+
118
+ server.registerTool(
119
+ 'driftseal_begin',
120
+ {
121
+ title: 'Begin a DriftSeal intent',
122
+ description:
123
+ 'Open one focused work-round intent before making repository changes. Fails if another intent is already open; close it explicitly first.',
124
+ inputSchema: {
125
+ intent: nonEmpty.describe('Outcome this work round will accomplish.'),
126
+ verify: nonEmpty.optional().describe('Exact command or outcome check that will prove completion.'),
127
+ decisions: z
128
+ .array(decisionId)
129
+ .default([])
130
+ .describe('Existing decision IDs this round may change or explicitly confirm.'),
131
+ },
132
+ outputSchema: { root: z.string(), intent: intentRecord },
133
+ annotations: localWrite,
134
+ },
135
+ async (input) =>
136
+ guarded(() => {
137
+ const intent = api.begin(input);
138
+ return success({ root: api.root, intent }, `Opened DriftSeal intent ${intent.id}.`);
139
+ })
140
+ );
141
+
142
+ server.registerTool(
143
+ 'driftseal_end',
144
+ {
145
+ title: 'Close a DriftSeal intent',
146
+ description:
147
+ '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.',
148
+ inputSchema: {
149
+ id: z.string().optional().describe('Intent ID; omit to close the current open intent.'),
150
+ status: closedStatus.default('completed'),
151
+ note: nonEmpty.optional().describe('What actually happened in the round.'),
152
+ verifyResult: nonEmpty.optional().describe('Concise, honest result of the declared verification.'),
153
+ },
154
+ outputSchema: { root: z.string(), intent: intentRecord },
155
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
156
+ },
157
+ async (input) =>
158
+ guarded(() => {
159
+ const intent = api.end(input);
160
+ return success({ root: api.root, intent }, `Closed DriftSeal intent ${intent.id} as ${intent.status}.`);
161
+ })
162
+ );
163
+
164
+ server.registerTool(
165
+ 'driftseal_log',
166
+ {
167
+ title: 'List DriftSeal intent history',
168
+ description:
169
+ 'Review recent or complete DriftSeal intent history to re-anchor work and understand prior outcomes. Reclaimed records are hidden unless includeReclaimed is set.',
170
+ inputSchema: {
171
+ last: z.number().int().positive().max(100).optional(),
172
+ includeReclaimed: z.boolean().default(false),
173
+ },
174
+ outputSchema: { root: z.string(), intents: z.array(intentRecord) },
175
+ annotations: readOnly,
176
+ },
177
+ async (input) =>
178
+ guarded(() => {
179
+ const intents = api.log({ last: input.last, all: input.includeReclaimed });
180
+ return success({ root: api.root, intents }, `Found ${intents.length} DriftSeal intent records.`);
181
+ })
182
+ );
183
+
184
+ server.registerTool(
185
+ 'driftseal_reclaim',
186
+ {
187
+ title: 'Reclaim DriftSeal intent records',
188
+ description:
189
+ 'Hide meaningless closed intent records (for example harness- or sandbox-caused failures) by appending reclaim markers. Never deletes log lines. Without ids, reclaims failed/abandoned, decision-unlinked records older than olderThan days.',
190
+ inputSchema: {
191
+ ids: z
192
+ .array(z.string())
193
+ .default([])
194
+ .describe('Intent IDs to reclaim; omit for batch mode by age.'),
195
+ reason: nonEmpty.describe('Why these records are meaningless (required, kept in the log).'),
196
+ olderThan: z
197
+ .number()
198
+ .int()
199
+ .positive()
200
+ .optional()
201
+ .describe('Batch mode retention window in days (default 7).'),
202
+ force: z
203
+ .boolean()
204
+ .default(false)
205
+ .describe('Allow reclaiming partial/completed or decision-linked records by explicit id.'),
206
+ dryRun: z.boolean().default(false),
207
+ },
208
+ outputSchema: { root: z.string(), intents: z.array(intentRecord) },
209
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
210
+ },
211
+ async (input) =>
212
+ guarded(() => {
213
+ const intents = api.reclaim({
214
+ ids: input.ids,
215
+ reason: input.reason,
216
+ olderThan: input.olderThan,
217
+ force: input.force,
218
+ dryRun: input.dryRun,
219
+ });
220
+ return success(
221
+ { root: api.root, intents },
222
+ input.dryRun
223
+ ? `${intents.length} DriftSeal intent records match.`
224
+ : `Reclaimed ${intents.length} DriftSeal intent records.`
225
+ );
226
+ })
227
+ );
228
+
229
+ server.registerTool(
230
+ 'driftseal_unreclaim',
231
+ {
232
+ title: 'Restore a reclaimed DriftSeal intent record',
233
+ description: 'Restore one reclaimed intent record to the visible log by appending an unreclaim marker.',
234
+ inputSchema: {
235
+ id: z.string(),
236
+ reason: nonEmpty.describe('Why this record is being restored (required, kept in the log).'),
237
+ },
238
+ outputSchema: { root: z.string(), intent: intentRecord },
239
+ annotations: localWrite,
240
+ },
241
+ async (input) =>
242
+ guarded(() => {
243
+ const intent = api.unreclaim(input);
244
+ return success({ root: api.root, intent }, `Restored DriftSeal intent ${intent.id}.`);
245
+ })
246
+ );
247
+
248
+ server.registerTool(
249
+ 'driftseal_decision_list',
250
+ {
251
+ title: 'List DriftSeal decisions',
252
+ description:
253
+ 'Find MADR decision records, optionally filtered by current status. Use this before showing or linking a decision.',
254
+ inputSchema: {
255
+ status: decisionStatus.optional(),
256
+ last: z.number().int().positive().max(100).optional(),
257
+ },
258
+ outputSchema: { root: z.string(), decisions: z.array(decisionRecord) },
259
+ annotations: readOnly,
260
+ },
261
+ async (input) =>
262
+ guarded(() => {
263
+ const decisions = api.decisionList(input);
264
+ return success({ root: api.root, decisions }, `Found ${decisions.length} DriftSeal decisions.`);
265
+ })
266
+ );
267
+
268
+ server.registerTool(
269
+ 'driftseal_decision_show',
270
+ {
271
+ title: 'Show a DriftSeal decision',
272
+ description: 'Read one complete MADR decision record by stable numeric ID.',
273
+ inputSchema: { id: decisionId },
274
+ outputSchema: { root: z.string(), decision: decisionWithContent },
275
+ annotations: readOnly,
276
+ },
277
+ async ({ id }) =>
278
+ guarded(() => {
279
+ const decision = api.decisionShow({ id });
280
+ return success({ root: api.root, decision }, `Loaded DriftSeal decision ${decision.id}.`);
281
+ })
282
+ );
283
+
284
+ server.registerTool(
285
+ 'driftseal_decision_add',
286
+ {
287
+ title: 'Add a DriftSeal decision',
288
+ description:
289
+ '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.',
290
+ inputSchema: {
291
+ title: nonEmpty,
292
+ context: nonEmpty,
293
+ outcome: nonEmpty,
294
+ status: decisionStatus.default('accepted'),
295
+ drivers: z.array(nonEmpty).default([]),
296
+ options: z.array(nonEmpty).default([]),
297
+ consequences: z.array(nonEmpty).default([]),
298
+ },
299
+ outputSchema: { root: z.string(), decision: decisionWithContent },
300
+ annotations: localWrite,
301
+ },
302
+ async (input) =>
303
+ guarded(() => {
304
+ const decision = api.decisionAdd(input);
305
+ return success({ root: api.root, decision }, `Created DriftSeal decision ${decision.id}.`);
306
+ })
307
+ );
308
+
309
+ server.registerTool(
310
+ 'driftseal_decision_update',
311
+ {
312
+ title: 'Reconcile a DriftSeal decision',
313
+ description:
314
+ 'Reconcile one decision linked to the current open intent, updating or explicitly confirming its status with a history note.',
315
+ inputSchema: {
316
+ id: decisionId,
317
+ status: decisionStatus.optional(),
318
+ note: nonEmpty,
319
+ },
320
+ outputSchema: { root: z.string(), decision: decisionWithContent },
321
+ annotations: localWrite,
322
+ },
323
+ async (input) =>
324
+ guarded(() => {
325
+ const decision = api.decisionUpdate(input);
326
+ return success({ root: api.root, decision }, `Reconciled DriftSeal decision ${decision.id}.`);
327
+ })
328
+ );
329
+ }
330
+
331
+ function registerResources(server, api) {
332
+ const registerJson = (name, uri, title, description, read) => {
333
+ server.registerResource(
334
+ name,
335
+ uri,
336
+ { title, description, mimeType: 'application/json' },
337
+ async () => {
338
+ const value = read();
339
+ return { contents: [{ uri, mimeType: 'application/json', text: jsonText(value) }] };
340
+ }
341
+ );
342
+ };
343
+
344
+ registerJson(
345
+ 'current-intent',
346
+ 'driftseal://intent/current',
347
+ 'Current DriftSeal intent',
348
+ 'The work-round intent currently in progress for the fixed repository.',
349
+ () => ({ root: api.root, intent: api.status() })
350
+ );
351
+ registerJson(
352
+ 'recent-intents',
353
+ 'driftseal://intents/recent',
354
+ 'Recent DriftSeal intents',
355
+ 'The ten most recent work-round intent records for the fixed repository.',
356
+ () => ({ root: api.root, intents: api.log({ last: 10 }) })
357
+ );
358
+ registerJson(
359
+ 'decision-catalog',
360
+ 'driftseal://decisions',
361
+ 'DriftSeal decision catalog',
362
+ 'All MADR decision summaries for the fixed repository.',
363
+ () => ({ root: api.root, decisions: api.decisionList() })
364
+ );
365
+ }
366
+
367
+ async function createServer({ root }) {
368
+ const [{ McpServer }, { StdioServerTransport }, zod] = await Promise.all([
369
+ import('@modelcontextprotocol/sdk/server/mcp.js'),
370
+ import('@modelcontextprotocol/sdk/server/stdio.js'),
371
+ import('zod'),
372
+ ]);
373
+ const z = zod.z || zod.default || zod;
374
+ const api = createApi({ root: path.resolve(root), isolateStorage: true });
375
+ const server = new McpServer(
376
+ { name: SERVER_NAME, version: SERVER_VERSION },
377
+ {
378
+ instructions:
379
+ '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.',
380
+ }
381
+ );
382
+ registerTools(server, api, z);
383
+ registerResources(server, api);
384
+ return { server, transport: new StdioServerTransport(), root: api.root };
385
+ }
386
+
387
+ async function main() {
388
+ const options = parseArguments(process.argv.slice(2));
389
+ if (options.help) {
390
+ process.stdout.write(helpText() + '\n');
391
+ return;
392
+ }
393
+ const { server, transport } = await createServer(options);
394
+ await server.connect(transport);
395
+ }
396
+
397
+ module.exports = { createServer, helpText, parseArguments, registerResources, registerTools };
398
+
399
+ if (require.main === module) {
400
+ main().catch((error) => {
401
+ const message = error && error.message ? error.message : String(error);
402
+ console.error(`driftseal-mcp: error: ${message}`);
403
+ process.exitCode = 1;
404
+ });
405
+ }