@rowan-hiro/inkan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hiro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,354 @@
1
+ # Inkan
2
+
3
+ [简体中文](README.zh.md)
4
+
5
+ **Agents make promises. Inkan keeps the receipts.**
6
+
7
+ *Seal what the work is meant to deliver. Then check that what landed is what was sealed.*
8
+
9
+ Inkan is a small, zero-dependency CLI for repositories where coding agents
10
+ do real work. It keeps a trustworthy record of what each piece of work was
11
+ meant to deliver, how that intent changed along the way, and what was
12
+ declared when it closed. It then binds each landing commit to that record,
13
+ so anyone, human or agent, can ask later whether the commit is faithful to
14
+ the promise.
15
+
16
+ It is also a working prototype of a larger idea: one record that follows an
17
+ agent's work through its whole lifecycle, from fixed intent to audited
18
+ commit, and is never rewritten along the way.
19
+
20
+ ## The problem
21
+
22
+ Long agent sessions lose the plot. Context gets compacted, a fresh session
23
+ picks up a half-finished task, and the task gets quietly reinterpreted:
24
+ scope narrows, a criterion is forgotten, A becomes B, and the final message
25
+ still says "done". Tests do not catch this. Tests tell you the code works.
26
+ They do not tell you it is the code you asked for.
27
+
28
+ The usual answer is more checking: more tests, more gates, re-verification
29
+ on every look at the log. That makes an agent re-check its own past work
30
+ every time it re-reads history, and the loop never ends. Inkan takes the
31
+ other route. It records what was declared and when, treats the record as
32
+ fact, and leaves judging the result to the repository's own tests.
33
+
34
+ ## Three questions
35
+
36
+ Inkan answers exactly three questions about a piece of work, and it refuses
37
+ to grow beyond them.
38
+
39
+ | Question | How Inkan answers it |
40
+ |---|---|
41
+ | Is the stored outcome still what was sealed? | One append-only file per outcome. A contract hash over the sealed text, its criteria, and every amendment. `inkan status` prints the seal verbatim with its hash. |
42
+ | Is what got committed the sealed outcome? | Closing records the hash of the working tree. The landing commit carries an `Inkan-Outcome: <id>` trailer. `inkan check <commit>` compares trailer, recorded hash, and recorded tree against the commit itself. |
43
+ | Was the outcome swapped mid-way without authorization? | The headline never changes. Criteria change only through `inkan amend --reason`. Closing requires a disposition for every live criterion. An outcome that was never closed stays visibly open; nothing closes it on anyone's behalf. |
44
+
45
+ ## Quick start
46
+
47
+ Requires Node.js 22 or newer and git.
48
+
49
+ ```sh
50
+ npm install --global @rowan-hiro/inkan
51
+ ```
52
+
53
+ This installs `inkan` and its alias `ink`. They accept identical arguments.
54
+
55
+ **1. Initialise the repository.**
56
+
57
+ ```sh
58
+ inkan init
59
+ ```
60
+
61
+ This writes a managed protocol block into `AGENTS.md` and creates `.inkan/`.
62
+ Commit both; they are part of the code from here on. Add `--claude` to also
63
+ link `CLAUDE.md` to `AGENTS.md`, so Claude Code reads the same policy from
64
+ the same file.
65
+
66
+ **2. Seal the outcome before touching code.**
67
+
68
+ ```sh
69
+ inkan begin "Ship account recovery" \
70
+ --accept "expired links are rejected" \
71
+ --accept "a valid link resets the password"
72
+ ```
73
+
74
+ ```
75
+ 2026-09-03-0621-82qz
76
+ ```
77
+
78
+ **3. When scope changes, amend. Never reinterpret.**
79
+
80
+ ```sh
81
+ inkan amend --reason "Security review asked for rate limiting" \
82
+ "Rate-limit recovery requests per account" \
83
+ --accept "more than five requests per hour are rejected"
84
+ ```
85
+
86
+ The original text stays. The amendment, its reason, and the new criterion
87
+ are appended, and the contract hash moves with them.
88
+
89
+ **4. Close with a disposition for every criterion.**
90
+
91
+ ```sh
92
+ inkan end --met 1 --met 2 --unmet 3 --note "Rate limiting deferred to the next sprint"
93
+ ```
94
+
95
+ ```
96
+ 2026-09-03-0621-82qz partial
97
+ Inkan-Outcome: 2026-09-03-0621-82qz
98
+ ```
99
+
100
+ Status is derived, not chosen: every criterion met is `completed`, any
101
+ unmet is `partial`. A truthful `partial` is a first-class result, and it is
102
+ what an agent reports instead of stretching the definition of done.
103
+
104
+ **5. Land it with the trailer.**
105
+
106
+ ```sh
107
+ git commit -m "$(printf 'feat: account recovery\n\nInkan-Outcome: 2026-09-03-0621-82qz\n')"
108
+ ```
109
+
110
+ The trailer goes in the last paragraph of the message, next to any other
111
+ trailers such as `Co-Authored-By`, with no blank line between them. Git
112
+ reads trailers only from that final paragraph; a trailer set apart by a
113
+ blank line is silently not a trailer.
114
+
115
+ **6. Ask, any time later, whether the commit kept its promise.**
116
+
117
+ ```sh
118
+ inkan check HEAD
119
+ ```
120
+
121
+ ```
122
+ b751a39 Inkan-Outcome: 2026-09-03-0621-82qz
123
+ outcome: present, closed (partial)
124
+ hash: matches refold
125
+ tree: matches commit tree
126
+ consistent
127
+ ```
128
+
129
+ ## What a swap looks like
130
+
131
+ Suppose the code is changed after the outcome closed and committed under
132
+ the same trailer. The recorded tree no longer matches the commit:
133
+
134
+ ```
135
+ c7f3a54 Inkan-Outcome: 2026-09-03-0621-82qz
136
+ outcome: present, closed (partial)
137
+ hash: matches refold
138
+ tree: differs from commit tree
139
+ mismatch
140
+ a mismatch is a fact about this commit; it is recorded, not repaired
141
+ ```
142
+
143
+ Exit codes are 0 for consistent, 1 for mismatch, and 2 when the commit
144
+ carries no `Inkan-Outcome` trailer at all. Nothing is repaired and nothing
145
+ is blocked. `check` is a report about the past, which is exactly why it can
146
+ run in a review or a CI job without turning into a gate.
147
+
148
+ ## After context loss
149
+
150
+ A new session, a new day, a compacted context: instead of guessing what was
151
+ in progress, ask.
152
+
153
+ ```sh
154
+ inkan status
155
+ inkan log -n 3
156
+ ```
157
+
158
+ ```
159
+ [2026-09-03-0621-82qz] open
160
+ sealed: 2026-09-03T06:21:06.511Z
161
+ hash: 9850337661df733ec923efc25bf9fdcb85ce30a3bb4cb3c07d7c84dd4fcaff56
162
+ outcome: Ship account recovery
163
+ 1. expired links are rejected
164
+ 2. a valid link resets the password
165
+ 3. more than five requests per hour are rejected
166
+ amend 2026-09-03T06:21:06.555Z: Security review asked for rate limiting
167
+ Rate-limit recovery requests per account
168
+ ```
169
+
170
+ An open outcome that is your work is the task at hand: continue it, or close
171
+ it with a note. One that is not your work belongs to another session: leave
172
+ it alone, tell the person it is there, and ask whether your work should run
173
+ in its own git worktree before beginning beside it. `log` prints one line
174
+ per outcome, newest first, so re-anchoring costs a few lines of context, not
175
+ a re-read of the history:
176
+
177
+ ```
178
+ 2026-09-03-0621-q51x completed Ship account recovery, second pass (1/1 met)
179
+ 2026-09-03-0621-82qz partial Ship account recovery (2/3 met)
180
+ ```
181
+
182
+ ## What Inkan refuses to do
183
+
184
+ These are the product, not its limitations.
185
+
186
+ - **It never runs anything.** No tests, no builds, no shell commands. The
187
+ only child process Inkan ever spawns is git, with a fixed argument list.
188
+ Whether the work is correct is the repository's job.
189
+ - **It never gates.** Nothing in Inkan runs before or during `git commit`,
190
+ and `init` installs no hooks. `check` and `doctor` report on commits and
191
+ files that already exist.
192
+ - **Closed is final.** There is no stale state, no invalidation, and no
193
+ notion that a closed outcome needs to be redone. Reviewing the log is
194
+ reading, not re-checking. If a past declaration now looks wrong, that is a
195
+ new outcome with its own seal.
196
+ - **It never closes an outcome on anyone's behalf.** Several outcomes can be
197
+ open at once, one per session or branch. `begin` names the others and
198
+ leaves them alone. An outcome that was never closed is an honest record of
199
+ exactly that; why it stayed open is a question for a person to investigate,
200
+ not a judgment for an agent to make. Closing for the sake of closing would
201
+ only fill the log with junk records.
202
+ - **The scenario is never rewritten.** An agent may challenge a decision when
203
+ circumstances change, by amendment or by a new decision record. It never
204
+ edits the text that records what was known and decided at the time.
205
+ - **No moving parts.** No server, no database, no index, no lock, no sidecar
206
+ file, no environment variable. Everything is plain text under `.inkan/`,
207
+ committed with the code, and merged by ordinary git.
208
+
209
+ ## Built for agents
210
+
211
+ `inkan init` writes a generated protocol block into `AGENTS.md`, the file
212
+ coding agents already read. Five rules: seal before durable changes; the
213
+ seal is a fact; close with dispositions, then commit with the trailer in the
214
+ last paragraph of the message; re-anchor with `inkan status` after context
215
+ loss and leave other sessions' outcomes alone; closed outcomes are final.
216
+ The block carries a protocol number. `init`
217
+ upgrades a block it generated under an earlier protocol in place and refuses
218
+ to overwrite a block that was edited by hand, so the policy lives in exactly
219
+ one place. `--lang <tag>` sets the language agents should write outcome
220
+ prose in. `inkan init --claude` also creates `CLAUDE.md` as a symlink to
221
+ `AGENTS.md`: Claude Code reads its own file name, and there is still one
222
+ policy, not a copy.
223
+
224
+ For agents that support skill files, the bundled `use-inkan` skill helps an
225
+ agent locate Inkan and re-anchor. It only points at `AGENTS.md`; it does not
226
+ restate or extend the protocol.
227
+
228
+ ```sh
229
+ inkan skill install # .agents/skills/use-inkan, read by most agents
230
+ inkan skill install --claude # .claude/skills/use-inkan, for Claude Code
231
+ inkan skill install --target <dir> # anywhere else, including a global directory
232
+ ```
233
+
234
+ ## Decisions travel with the code
235
+
236
+ Design choices are recorded as MADR (Markdown Architectural Decision
237
+ Records) under `.inkan/decisions/`, one numbered `NNNN-slug.md` file each.
238
+ `inkan decision add` writes one. Its Context and Decision Outcome sections
239
+ record the scenario and the choice at the time, and they are never edited
240
+ afterwards. To challenge a decision, `inkan decision update <id> --status
241
+ <status> --reason "<text>"` appends a dated history entry, or a new record
242
+ supersedes the old one.
243
+
244
+ An outcome names the decisions it is bound by with `--decision <id>` on
245
+ `begin` or `amend`. They are constraints on the work, never a gate on
246
+ closing it.
247
+
248
+ Inkan's own design is recorded this way, from the boundary in `0001`
249
+ onward. There is no separate design document; `inkan decision list` prints
250
+ the index.
251
+
252
+ ## A prototype for the whole lifecycle
253
+
254
+ Inkan is also a working prototype of something larger: managing an agent's
255
+ work across its whole lifecycle, from the moment intent is fixed to the
256
+ moment a commit is audited, with one record that every stage writes to and
257
+ no stage rewrites.
258
+
259
+ | Stage | What is recorded | Command |
260
+ |---|---|---|
261
+ | Intent | What will be delivered and how it will be judged | `begin` |
262
+ | Change | How the intent moved, and why | `amend --reason` |
263
+ | Constraint | The decisions the work is bound by | `decision add`, `--decision` |
264
+ | Close | A disposition per criterion and a derived, truthful status | `end` |
265
+ | Delivery | The commit that landed the work, bound to the seal | `Inkan-Outcome` trailer |
266
+ | Audit | Whether the commit and the record still agree | `check`, `doctor` |
267
+ | Resume | Where a fresh session picks up | `status`, `log` |
268
+
269
+ Inkan is built this way itself. Every commit in its repository after the
270
+ first carries an `Inkan-Outcome` trailer, each milestone was sealed and
271
+ closed as an outcome, and its design decisions live in `.inkan/decisions/`.
272
+ The record is deliberately small. What the prototype tests is whether a
273
+ lifecycle can be managed by a log of facts alone, with no runner and no
274
+ gate in the loop.
275
+
276
+ ## Command reference
277
+
278
+ | Command | Effect | Refuses when |
279
+ |---|---|---|
280
+ | `inkan init [--lang <tag>] [--claude]` | Writes or upgrades the managed block in `AGENTS.md`; creates `.inkan/`. `--claude` also links `CLAUDE.md` to `AGENTS.md`. | The block was hand-edited. A `CLAUDE.md` exists that is not that symlink. |
281
+ | `inkan begin "<outcome>" [--accept <text>]... [--decision <id>]... [--lane <tag>]` | Seals a new outcome; prints its id. Any other open outcome is named in a notice on stderr and left untouched. | Never. |
282
+ | `inkan amend --reason <text> [<addition>] [--accept <text>]... [--withdraw <n>]... [--decision <id>]... [<id>]` | Appends an amendment; prints the new contract hash. | No reason. No open outcome. Ambiguous open outcome without `<id>`. |
283
+ | `inkan end [<id>] [--met <n>]... [--unmet <n>]... [-s abandoned] --note <text>` | Records dispositions and closes. Status is derived: all met is `completed`, any unmet is `partial`. Prints the commit trailer line. | A live criterion has no disposition, unless closing with `-s abandoned`. No note. |
284
+ | `inkan status` | Prints every open outcome verbatim: sealed time, hash, lane, criteria with indexes, amendments with reasons, linked decisions. | Never. |
285
+ | `inkan log [-n N] [--since <date>] [--grep <regex>] [--status <s>] [--decision <id>] [--lane <tag>] [<id>]` | One line per outcome, newest first, default 20. `<id>` prints one outcome in full, including dispositions, note, and recorded tree. Filters combine. | Never. |
286
+ | `inkan check [<commit>]` | Read-only. Reads `Inkan-Outcome` trailers, loads each named outcome from the commit's own tree, refolds it, and reports trailer, closure, hash, and tree. Exit 0 consistent, 1 mismatch, 2 no trailer. | Never blocks anything. |
287
+ | `inkan doctor` | Read-only. Folds every outcome and parses every decision; reports corrupt files, id mismatches, duplicate decision ids, and dangling decision links. Exit 0 clean, 1 problems. | Never. |
288
+ | `inkan decision add "<title>" --context <text> --decision <text> [--driver <text>]... [--option <text>]... [--consequence <text>]... [-s <status>]` | Writes a numbered MADR file; prints its path. | Missing required sections. |
289
+ | `inkan decision update <id> --status <status> --reason <text>` | Appends a dated history entry and sets the new status. Names the open outcome when there is one. Never edits Context or Decision Outcome. | Unknown id or status. |
290
+ | `inkan decision list [-s <status>]` / `inkan decision show <id>` | Read-only. `show` accepts `2`, `02`, or `0002`. | Never. |
291
+ | `inkan skill install [--claude \| --target <dir>]` | Copies the bundled skill to `.agents/skills/use-inkan/` under the repository root, to `.claude/skills/use-inkan/` with `--claude`, or to `<dir>/use-inkan/`; prints the destination. | The destination exists and differs from the bundled skill. `--claude` with `--target`. |
292
+
293
+ Decision statuses are `proposed`, `accepted`, `rejected`, `deferred`,
294
+ `deprecated`, and `superseded`.
295
+
296
+ ## How it works
297
+
298
+ ```
299
+ .inkan/
300
+ outcomes/<id>.jsonl one append-only file per outcome
301
+ decisions/NNNN-slug.md MADR records
302
+ ```
303
+
304
+ An outcome id such as `2026-09-03-1432-k7m2` is the UTC date and minute the
305
+ outcome was begun plus four random characters, so ids sort chronologically
306
+ and two branches essentially never collide. Each outcome file holds a
307
+ `begin` event, any `amend` events, and at most one `end` event. An open
308
+ outcome is simply a file with no `end` yet.
309
+
310
+ The contract hash is a SHA-256 over the outcome text, its criteria with
311
+ their withdrawn flags, the linked decisions, and every amendment's reason
312
+ and addition. `end` records it, together with the git tree hash of the
313
+ working tree with `.inkan/outcomes` excluded, so the log never perturbs the
314
+ hash of the work it describes. `check` refolds the outcome as stored in the
315
+ commit and compares both.
316
+
317
+ Because each outcome is its own file, two branches never touch the same
318
+ file and an ordinary merge brings them together. It also keeps review cheap
319
+ without a cache: an unfiltered `log` reads only as many files as it prints, and the bundled
320
+ benchmark (`npm run bench`) seeds ten thousand closed outcomes and holds
321
+ `log -n 3` under 50 ms, `log --grep` under 1 s, and `doctor` under 2 s.
322
+
323
+ ## Status
324
+
325
+ Inkan 0.1.0 is the first release, rebuilt from scratch as the successor to
326
+ DriftSeal. Deliberately not in this release: an importer for DriftSeal
327
+ history and an MCP server. Those are adapters and can follow without
328
+ changing the record format. The only host-specific convenience is
329
+ `--claude` on `init` and `skill install`; every other host reads
330
+ `AGENTS.md` and `.agents/skills` as they are. Lanes exist only as an
331
+ optional filing tag on `begin` and a filter on `log`.
332
+
333
+ ## Releasing
334
+
335
+ Every push to `main` and every pull request runs the test suite on Node 22,
336
+ 24, and 26, the history benchmark, and a package smoke on Linux, macOS, and
337
+ Windows that packs the tarball, installs it, and drives the installed
338
+ `inkan` through a full outcome (`npm run test:package`).
339
+
340
+ A release is a GitHub Release whose tag is `v` plus the version in
341
+ `package.json`. Publishing the release runs the publish workflow, which
342
+ checks that the tag and the version agree, runs the tests and the package
343
+ smoke again, and publishes to npm with trusted publishing (OIDC). No npm
344
+ token is stored anywhere. The one-time setup is on npmjs.com, in the
345
+ package's settings: add a trusted publisher of type GitHub Actions with
346
+ owner `rowan-hiro`, repository `inkan`, and workflow `publish.yml`. If
347
+ npm does not let a trusted publisher be added to a package that has never
348
+ been published, publish the first release by hand from a checkout of the
349
+ tag with `npm publish --access public`; every release after that goes
350
+ through the workflow.
351
+
352
+ ## License
353
+
354
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,239 @@
1
+ # Inkan
2
+
3
+ [English](README.md) | 简体中文
4
+
5
+ **Agent 许下承诺,Inkan 留下凭据。**
6
+
7
+ *先把工作真正要交付的结果 seal 下来,再核对最终落地的内容有没有兑现承诺。*
8
+
9
+ Inkan 是一款小巧、零依赖的 CLI,专为让 coding agent 真正参与开发的代码仓库而生。它会可靠地记录每项工作原本要交付什么、意图在过程中如何变化,以及工作结束时作出了怎样的声明;再把最终落地的每个 commit 与这份记录绑定。于是,无论人还是 agent,日后都能清楚回答:这个 commit,真的兑现了当初的承诺吗?
10
+
11
+ 它同时也是一个更宏大构想的可运行原型:用一份贯穿 agent 工作全生命周期的记录,从意图确定一路走到 commit 审计,并且全程只追加、不改写。
12
+
13
+ ## 问题所在
14
+
15
+ 漫长的 agent session 很容易偏离目标。Context 被压缩后,新 session 接手一项做到一半的任务,任务却在不知不觉间被重新解释:范围缩小了,一条验收标准被忘了,A 变成了 B,最终消息却依然写着“完成”。测试发现不了这种问题。测试能告诉你代码是否可用,却不能告诉你,这是不是你最初要求的那份代码。
16
+
17
+ 常见的解决办法是不断增加检查:更多测试、更多 gate,每次查看日志都重新验证一遍。这会让 agent 每次重读历史时,都再次检查自己过去的工作,最终陷入没有尽头的循环。Inkan 选择了另一条路:如实记录当时声明了什么、声明发生在何时,把记录本身视为事实,并把结果是否正确的判断交还给仓库自己的测试体系。
18
+
19
+ ## 三个问题
20
+
21
+ 对于一项工作,Inkan 只回答三个问题,并且刻意不越界。
22
+
23
+ | 问题 | Inkan 如何回答 |
24
+ |---|---|
25
+ | 保存下来的 outcome 还是当初 seal 的那一个吗? | 每个 outcome 都有自己的 append-only 文件。系统会根据 seal 的文本、验收标准和历次 amendment 计算 contract hash。`inkan status` 会逐字打印 seal 及其 hash。 |
26
+ | commit 中落地的内容就是 seal 的 outcome 吗? | 关闭 outcome 时会记录 working tree 的 hash。落地 commit 带有 `Inkan-Outcome: <id>` trailer。`inkan check <commit>` 会把 trailer、记录的 hash 和记录的 tree 与 commit 本身逐一比对。 |
27
+ | outcome 是否在中途被擅自调换? | 标题永远不变。验收标准只能通过 `inkan amend --reason` 修改。关闭时,每一条仍生效的标准都必须给出 disposition。从未关闭的 outcome 会一直明确显示为 open;Inkan 不会替任何人关闭它。 |
28
+
29
+ ## 快速开始
30
+
31
+ 需要 Node.js 22 或更高版本,以及 git。
32
+
33
+ ```sh
34
+ npm install --global @rowan-hiro/inkan
35
+ ```
36
+
37
+ 这会安装 `inkan` 及其别名 `ink`,两者接受完全相同的参数。
38
+
39
+ **1. 初始化仓库。**
40
+
41
+ ```sh
42
+ inkan init
43
+ ```
44
+
45
+ 这条命令会把一段由 Inkan 管理的 protocol 写入 `AGENTS.md`,并创建 `.inkan/`。请把两者一并 commit;从这一刻开始,它们就是代码的一部分。加上 `--claude`,还会把 `CLAUDE.md` 软链到 `AGENTS.md`,让 Claude Code 从同一个文件读取同一份 policy。
46
+
47
+ **2. 在动手修改代码之前 seal outcome。**
48
+
49
+ ```sh
50
+ inkan begin "Ship account recovery" \
51
+ --accept "expired links are rejected" \
52
+ --accept "a valid link resets the password"
53
+ ```
54
+
55
+ ```
56
+ 2026-09-03-0621-82qz
57
+ ```
58
+
59
+ **3. 范围发生变化时,明确 amend,绝不重新解释。**
60
+
61
+ ```sh
62
+ inkan amend --reason "Security review asked for rate limiting" \
63
+ "Rate-limit recovery requests per account" \
64
+ --accept "more than five requests per hour are rejected"
65
+ ```
66
+
67
+ 原始文本会完整保留。amendment、变更原因和新增标准会被追加到记录中,contract hash 也会随之更新。
68
+
69
+ **4. 关闭时,为每一条标准给出 disposition。**
70
+
71
+ ```sh
72
+ inkan end --met 1 --met 2 --unmet 3 --note "Rate limiting deferred to the next sprint"
73
+ ```
74
+
75
+ ```
76
+ 2026-09-03-0621-82qz partial
77
+ Inkan-Outcome: 2026-09-03-0621-82qz
78
+ ```
79
+
80
+ 状态由事实推导,而不是人为选择:所有标准均达成时为 `completed`,只要有一项未达成就是 `partial`。如实记录的 `partial` 是一种完整、正式的结果;agent 应当报告它,而不是为了声称“完成”而拉伸 done 的定义。
81
+
82
+ **5. 带上 trailer,让工作落地。**
83
+
84
+ ```sh
85
+ git commit -m "$(printf 'feat: account recovery\n\nInkan-Outcome: 2026-09-03-0621-82qz\n')"
86
+ ```
87
+
88
+ trailer 必须放在 commit message 的最后一个段落,并与 `Co-Authored-By` 等其他 trailer 相邻,中间不能有空行。Git 只会把最后一个段落中的内容识别为 trailer;用空行单独隔开的 trailer,不会报错,却也不会被识别。
89
+
90
+ **6. 日后随时检查:这个 commit 是否兑现了承诺?**
91
+
92
+ ```sh
93
+ inkan check HEAD
94
+ ```
95
+
96
+ ```
97
+ b751a39 Inkan-Outcome: 2026-09-03-0621-82qz
98
+ outcome: present, closed (partial)
99
+ hash: matches refold
100
+ tree: matches commit tree
101
+ consistent
102
+ ```
103
+
104
+ ## 偷换 outcome 会是什么样
105
+
106
+ 假设 outcome 关闭后,代码又被修改,却仍使用同一个 trailer 提交。此时,记录中的 tree 将不再匹配这个 commit:
107
+
108
+ ```
109
+ c7f3a54 Inkan-Outcome: 2026-09-03-0621-82qz
110
+ outcome: present, closed (partial)
111
+ hash: matches refold
112
+ tree: differs from commit tree
113
+ mismatch
114
+ a mismatch is a fact about this commit; it is recorded, not repaired
115
+ ```
116
+
117
+ 一致时退出码为 0,不匹配时为 1;如果 commit 完全没有 `Inkan-Outcome` trailer,则为 2。Inkan 不会修复任何内容,也不会阻塞任何操作。`check` 只是对过去作出报告——也正因为如此,它可以安心地运行在 code review 或 CI job 中,而不会摇身变成一道 gate。
118
+
119
+ ## Context 丢失之后
120
+
121
+ 新的 session、新的一天,或一次 context compaction 之后,不要靠猜来判断正在做什么,直接查询记录。
122
+
123
+ ```sh
124
+ inkan status
125
+ inkan log -n 3
126
+ ```
127
+
128
+ ```
129
+ [2026-09-03-0621-82qz] open
130
+ sealed: 2026-09-03T06:21:06.511Z
131
+ hash: 9850337661df733ec923efc25bf9fdcb85ce30a3bb4cb3c07d7c84dd4fcaff56
132
+ outcome: Ship account recovery
133
+ 1. expired links are rejected
134
+ 2. a valid link resets the password
135
+ 3. more than five requests per hour are rejected
136
+ amend 2026-09-03T06:21:06.555Z: Security review asked for rate limiting
137
+ Rate-limit recovery requests per account
138
+ ```
139
+
140
+ 如果一个 open outcome 属于你的工作,它就是当前任务:继续完成它,或者写明 note 后关闭。如果它不属于你,那就是另一个 session 的工作:不要动它;告诉负责人这里已有一项 open outcome,并在与它并行开始新工作前,询问是否应该使用独立的 git worktree。`log` 按从新到旧的顺序,每个 outcome 只打印一行。因此,重新锚定上下文只需要寥寥几行,而不必重读全部历史:
141
+
142
+ ```
143
+ 2026-09-03-0621-q51x completed Ship account recovery, second pass (1/1 met)
144
+ 2026-09-03-0621-82qz partial Ship account recovery (2/3 met)
145
+ ```
146
+
147
+ ## Inkan 坚决不做什么
148
+
149
+ 这些不是功能缺失,而是产品本身的边界。
150
+
151
+ - **它绝不运行任何任务。** 不运行测试、build 或 shell command。Inkan 唯一会启动的 child process 是 git,而且参数列表固定。工作是否正确,应由仓库自己判断。
152
+ - **它绝不充当 gate。** `git commit` 之前或期间不会运行任何 Inkan 操作,`init` 也不会安装 hook。`check` 和 `doctor` 只报告已经存在的 commit 和文件。
153
+ - **关闭即最终状态。** 没有 stale state,没有 invalidation,也不存在已经关闭的 outcome 还需要重做的概念。查看日志就是阅读,而不是重新检查。如果过去的声明如今看来有误,那应当成为一项拥有独立 seal 的新 outcome。
154
+ - **它绝不代替别人关闭 outcome。** 多个 outcome 可以同时 open,每个 session 或 branch 各自拥有一个。`begin` 会指出其他 outcome 的存在,但不会碰它们。从未关闭的 outcome,就是“它确实没有关闭”的诚实记录;为何一直 open,应由人来调查,而不是由 agent 擅自判断。仅仅为了关闭而关闭,只会让日志充斥无意义的记录。
155
+ - **它绝不改写当时的场景。** 情况变化时,agent 可以通过 amendment 或新的 decision record 对既有决定提出挑战,但绝不会修改那段记录了当时所知信息与所作决定的文本。
156
+ - **没有额外运转部件。** 没有 server、database、index、lock、sidecar file 或 environment variable。一切都是 `.inkan/` 下的纯文本,随代码一同 commit,并通过普通 git 操作完成 merge。
157
+
158
+ ## 为 agent 而生
159
+
160
+ `inkan init` 会把生成好的 protocol block 写进 coding agent 本来就会读取的 `AGENTS.md`。其中只有五条规则:在 durable change 之前 seal;seal 是事实;先逐项 disposition 并关闭,再把 trailer 放在 commit message 的最后一段完成提交;context 丢失后用 `inkan status` 重新锚定,同时不碰其他 session 的 outcome;关闭即最终状态。
161
+
162
+ protocol block 带有版本号。`init` 会原地升级由旧版 protocol 生成的 block,但拒绝覆盖经过手工编辑的 block,确保 policy 始终只有一个权威来源。`--lang <tag>` 用来设置 agent 撰写 outcome 文本时应使用的语言。`inkan init --claude` 还会把 `CLAUDE.md` 创建为指向 `AGENTS.md` 的 symlink:Claude Code 读的是自己认识的文件名,而 policy 依然只有一份,不是副本。
163
+
164
+ 对于支持 skill 文件的 agent,Inkan 内置的 `use-inkan` skill 可以帮助 agent 定位 Inkan 并重新锚定。它只会指向 `AGENTS.md`,不会复述或扩展 protocol。
165
+
166
+ ```sh
167
+ inkan skill install # .agents/skills/use-inkan,大多数 agent 读取的路径
168
+ inkan skill install --claude # .claude/skills/use-inkan,给 Claude Code
169
+ inkan skill install --target <dir> # 其他任何位置,包括全局目录
170
+ ```
171
+
172
+ ## 让决策与代码同行
173
+
174
+ 设计选择以 MADR(Markdown Architectural Decision Records)的形式保存在 `.inkan/decisions/` 下,每条记录对应一个编号为 `NNNN-slug.md` 的文件。`inkan decision add` 用于创建记录。其中的 Context 和 Decision Outcome 部分会记下当时的场景与选择,之后永不改写。
175
+
176
+ 如果要挑战一项决定,可运行 `inkan decision update <id> --status <status> --reason "<text>"`,追加一条带日期的历史记录;也可以创建一条新记录来 supersede 旧记录。
177
+
178
+ outcome 可以在 `begin` 或 `amend` 时通过 `--decision <id>` 指明自己受哪些 decision 约束。这些 decision 是工作的约束条件,但绝不是关闭工作的 gate。
179
+
180
+ Inkan 自己的设计也用同样的方式记录,从 `0001` 中划定的边界一路延续至今。仓库没有单独的设计文档;`inkan decision list` 就是它的索引。
181
+
182
+ ## 面向完整生命周期的原型
183
+
184
+ Inkan 还是一个更大构想的可运行原型:管理 agent 工作的完整生命周期,从意图确定的那一刻,到 commit 接受审计的那一刻。整个过程中,每个阶段都向同一份记录写入事实,没有任何阶段会改写过去。
185
+
186
+ | 阶段 | 记录什么 | 命令 |
187
+ |---|---|---|
188
+ | 意图 | 要交付什么,以及如何判断是否达成 | `begin` |
189
+ | 变更 | 意图如何变化,以及为什么变化 | `amend --reason` |
190
+ | 约束 | 当前工作受哪些 decision 约束 | `decision add`、`--decision` |
191
+ | 关闭 | 每条标准的 disposition,以及由此推导出的诚实状态 | `end` |
192
+ | 交付 | 真正落地的 commit,并与 seal 绑定 | `Inkan-Outcome` trailer |
193
+ | 审计 | commit 与记录是否仍然一致 | `check`、`doctor` |
194
+ | 恢复 | 新 session 从哪里接手 | `status`、`log` |
195
+
196
+ Inkan 本身就是这样构建的。仓库从第二个 commit 开始,每个 commit 都带有 `Inkan-Outcome` trailer;每个 milestone 都先作为 outcome 被 seal,随后关闭;所有设计决定都保存在 `.inkan/decisions/` 中。
197
+
198
+ 这份记录被刻意设计得很小。这个原型真正要验证的是:不在流程里加入 runner,也不设置 gate,仅凭一份只记录事实的日志,能否管理完整的生命周期。
199
+
200
+ ## 命令参考
201
+
202
+ | 命令 | 作用 | 何时拒绝执行 |
203
+ |---|---|---|
204
+ | `inkan init [--lang <tag>] [--claude]` | 写入或升级 `AGENTS.md` 中由 Inkan 管理的 block;创建 `.inkan/`。`--claude` 还会把 `CLAUDE.md` 软链到 `AGENTS.md`。 | block 曾被手工编辑;已存在一个不是该 symlink 的 `CLAUDE.md`。 |
205
+ | `inkan begin "<outcome>" [--accept <text>]... [--decision <id>]... [--lane <tag>]` | Seal 一个新 outcome,并打印其 id。其他 open outcome 会在 stderr 的 notice 中被点名,但不会受到任何改动。 | 永不拒绝。 |
206
+ | `inkan amend --reason <text> [<addition>] [--accept <text>]... [--withdraw <n>]... [--decision <id>]... [<id>]` | 追加 amendment,并打印新的 contract hash。 | 没有 reason;没有 open outcome;存在多个 open outcome,却没有用 `<id>` 明确指定目标。 |
207
+ | `inkan end [<id>] [--met <n>]... [--unmet <n>]... [-s abandoned] --note <text>` | 记录 disposition 并关闭 outcome。状态由结果推导:全部 met 为 `completed`,任一 unmet 为 `partial`。打印 commit trailer 行。 | 仍生效的标准缺少 disposition(以 `-s abandoned` 关闭时除外);没有 note。 |
208
+ | `inkan status` | 逐字打印所有 open outcome:seal 时间、hash、lane、带编号的标准、附 reason 的 amendment,以及关联的 decision。 | 永不拒绝。 |
209
+ | `inkan log [-n N] [--since <date>] [--grep <regex>] [--status <s>] [--decision <id>] [--lane <tag>] [<id>]` | 每个 outcome 打印一行,最新的在前,默认 20 条。`<id>` 会完整打印一项 outcome,包括 disposition、note 和记录的 tree。filter 可以组合。 | 永不拒绝。 |
210
+ | `inkan check [<commit>]` | 只读。从 commit 自身的 tree 中读取 `Inkan-Outcome` trailer 和对应 outcome,重新 fold 后报告 trailer、关闭状态、hash 与 tree。退出码:一致为 0,不匹配为 1,没有 trailer 为 2。 | 不会阻塞任何操作。 |
211
+ | `inkan doctor` | 只读。Fold 所有 outcome 并解析所有 decision;报告损坏文件、id 不匹配、重复的 decision id,以及失效的 decision link。退出码:正常为 0,发现问题为 1。 | 永不拒绝。 |
212
+ | `inkan decision add "<title>" --context <text> --decision <text> [--driver <text>]... [--option <text>]... [--consequence <text>]... [-s <status>]` | 写入一个带编号的 MADR 文件,并打印其路径。 | 缺少必要 section。 |
213
+ | `inkan decision update <id> --status <status> --reason <text>` | 追加一条带日期的历史记录,并设置新状态。有 open outcome 时会指出它的名称。永不编辑 Context 或 Decision Outcome。 | id 或 status 未知。 |
214
+ | `inkan decision list [-s <status>]` / `inkan decision show <id>` | 只读。`show` 接受 `2`、`02` 或 `0002`。 | 永不拒绝。 |
215
+ | `inkan skill install [--claude \| --target <dir>]` | 把内置 skill 复制到仓库根目录下的 `.agents/skills/use-inkan/`;加 `--claude` 时复制到 `.claude/skills/use-inkan/`;指定 `--target` 时复制到 `<dir>/use-inkan/`。打印目标路径。 | 目标已存在,且与内置 skill 不同;`--claude` 与 `--target` 同时给出。 |
216
+
217
+ Decision status 包括 `proposed`、`accepted`、`rejected`、`deferred`、`deprecated` 和 `superseded`。
218
+
219
+ ## 工作原理
220
+
221
+ ```
222
+ .inkan/
223
+ outcomes/<id>.jsonl 每个 outcome 一个 append-only 文件
224
+ decisions/NNNN-slug.md MADR 记录
225
+ ```
226
+
227
+ `2026-09-03-1432-k7m2` 这样的 outcome id,由 outcome 开始时的 UTC 日期与分钟,加上四个随机字符组成。因此 id 可以按时间排序,两个 branch 也几乎不可能发生冲突。每个 outcome 文件包含一个 `begin` event、任意数量的 `amend` event,以及最多一个 `end` event。所谓 open outcome,就是一个尚无 `end` 的文件。
228
+
229
+ contract hash 是一个 SHA-256,计算范围包括 outcome 文本、带 withdrawn 标记的验收标准、关联的 decision,以及每次 amendment 的 reason 和 addition。`end` 会记录这个 hash,同时记录 working tree 的 git tree hash;计算时会排除 `.inkan/outcomes`,因此日志本身不会扰动它所描述的工作 hash。`check` 会重新 fold commit 中保存的 outcome,并同时比较两者。
230
+
231
+ 由于每个 outcome 都有独立文件,两个 branch 永远不会改动同一个 outcome 文件,普通 merge 就能把记录自然汇合。这种设计不需要 cache,也能让 review 保持轻快:不带 filter 的 `log` 只读取实际要打印的文件数;内置 benchmark(`npm run bench`)会生成一万个已关闭的 outcome,并将 `log -n 3` 控制在 50 ms 以内、`log --grep` 控制在 1 秒以内、`doctor` 控制在 2 秒以内。
232
+
233
+ ## 当前状态
234
+
235
+ Inkan 0.1.0 是首个正式版本,也是 DriftSeal 继任者的一次从零重构。本版本有意没有加入 DriftSeal 历史记录 importer 和 MCP server。这些都属于 adapter,可以后续补上,而无需改变记录格式。唯一针对特定 host 的便利是 `init` 和 `skill install` 的 `--claude`;其他 host 直接读取 `AGENTS.md` 和 `.agents/skills`,无需任何适配。Lane 目前只作为 `begin` 时可选的归档 tag,以及 `log` 的 filter。
236
+
237
+ ## License
238
+
239
+ MIT
package/bin/ink.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/cli.js';
package/bin/inkan.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/cli.js';
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@rowan-hiro/inkan",
3
+ "version": "0.1.0",
4
+ "description": "Seal the authoritative outcome; keep a trustworthy record of the process.",
5
+ "type": "module",
6
+ "bin": {
7
+ "inkan": "bin/inkan.js",
8
+ "ink": "bin/ink.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=22"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Hiro <rowan_hiro@proton.me>",
15
+ "homepage": "https://github.com/rowan-hiro/inkan#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/rowan-hiro/inkan/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/rowan-hiro/inkan.git"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "src",
26
+ "skills",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "test": "node --test test/*.test.js",
32
+ "bench": "node bench/history.js",
33
+ "test:package": "node scripts/test-package.mjs"
34
+ },
35
+ "keywords": [
36
+ "inkan",
37
+ "agents",
38
+ "agentic-coding",
39
+ "outcome-log",
40
+ "decision-log",
41
+ "madr"
42
+ ]
43
+ }