@rowan-hiro/inkan 0.1.0 → 0.2.1

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
@@ -4,18 +4,14 @@
4
4
 
5
5
  **Agents make promises. Inkan keeps the receipts.**
6
6
 
7
- *Seal what the work is meant to deliver. Then check that what landed is what was sealed.*
7
+ *Seal what the work is meant to deliver. Record what was declared when it closed.*
8
8
 
9
9
  Inkan is a small, zero-dependency CLI for repositories where coding agents
10
10
  do real work. It keeps a trustworthy record of what each piece of work was
11
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.
12
+ declared when it closed. The record lives in the repository and travels
13
+ with the code, so a fresh session or a different agent can pick up the work
14
+ without relying on another assistant's memory.
19
15
 
20
16
  ## The problem
21
17
 
@@ -31,20 +27,22 @@ every time it re-reads history, and the loop never ends. Inkan takes the
31
27
  other route. It records what was declared and when, treats the record as
32
28
  fact, and leaves judging the result to the repository's own tests.
33
29
 
34
- ## Three questions
35
-
36
- Inkan answers exactly three questions about a piece of work, and it refuses
37
- to grow beyond them.
30
+ ## What the record holds
38
31
 
39
32
  | Question | How Inkan answers it |
40
33
  |---|---|
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. |
34
+ | What was the work meant to deliver? | `begin` seals the outcome text and its acceptance criteria in an append-only file. `status` prints open outcomes verbatim. |
35
+ | How did the intent change? | The headline stays. `amend --reason` appends the reason, additions, and any added or withdrawn criteria. |
36
+ | What was declared at close? | `end` records a disposition for every live criterion and a note. An outcome without an end event stays open. |
37
+
38
+ These are declarations. Inkan does not compare them with commits or decide
39
+ whether the work fulfilled the criteria. Closing creates no later audit
40
+ step and no obligation to prove the same work again.
44
41
 
45
42
  ## Quick start
46
43
 
47
- Requires Node.js 22 or newer and git.
44
+ Requires Node.js 22 or newer. Commit the records with your usual Git workflow;
45
+ Inkan itself does not invoke Git.
48
46
 
49
47
  ```sh
50
48
  npm install --global @rowan-hiro/inkan
@@ -97,53 +95,44 @@ inkan end --met 1 --met 2 --unmet 3 --note "Rate limiting deferred to the next s
97
95
  Inkan-Outcome: 2026-09-03-0621-82qz
98
96
  ```
99
97
 
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
- ```
98
+ Status is derived from the dispositions: every criterion declared met is
99
+ `completed`, any declared unmet is `partial`. A truthful `partial` is a
100
+ first-class result, and it is what an agent reports instead of stretching
101
+ the definition of done.
109
102
 
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.
103
+ **5. Commit the work and its record together.**
114
104
 
115
- **6. Ask, any time later, whether the commit kept its promise.**
105
+ Stage the files for this outcome, including its `.inkan/` records. Include
106
+ the trailer printed by `end` in the landing commit:
116
107
 
117
108
  ```sh
118
- inkan check HEAD
109
+ git commit -m "feat: account recovery" \
110
+ -m "Inkan-Outcome: 2026-09-03-0621-82qz"
119
111
  ```
120
112
 
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
- ```
113
+ The trailer goes in the final paragraph of the commit message, beside any
114
+ other trailers without a blank line between them. The agent protocol
115
+ requires it when submitting the work; Inkan installs no hook or executable
116
+ gate to enforce that writing rule.
128
117
 
129
- ## What a swap looks like
118
+ The outcome is closed. There is no delivery audit to run afterwards.
130
119
 
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:
120
+ ## Commit references when reading history
133
121
 
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
- ```
122
+ `Inkan-Outcome: <id>` associates a commit with an outcome. It supplies
123
+ context: what the work was meant to deliver, how it changed, and what was
124
+ declared at close. It certifies neither completion nor acceptance criteria
125
+ nor the contents of the commit.
142
126
 
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.
127
+ Follow the reference when useful. For historical context, prefer the file
128
+ `.inkan/outcomes/<id>.jsonl` stored in that commit, readable with ordinary
129
+ Git. `inkan log <id>` reads the record in the current checkout. These reads
130
+ supply context without comparing the record with the commit.
131
+
132
+ Missing trailers or unavailable referenced records are missing information.
133
+ Reading history does not require validating delivery, backfilling trailers,
134
+ repairing old commits, or reopening closed outcomes. The writing requirement
135
+ applies when making new commits; it creates no retroactive duty for readers.
147
136
 
148
137
  ## After context loss
149
138
 
@@ -184,11 +173,11 @@ a re-read of the history:
184
173
  These are the product, not its limitations.
185
174
 
186
175
  - **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.
176
+ tool spawns no child processes, including Git. Whether the work is
177
+ correct is the repository's job.
189
178
  - **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.
179
+ and `init` installs no hooks. There is no commit comparison or delivery
180
+ audit. `doctor` is an optional file diagnostic, never a workflow step.
192
181
  - **Closed is final.** There is no stale state, no invalidation, and no
193
182
  notion that a closed outcome needs to be redone. Reviewing the log is
194
183
  reading, not re-checking. If a past declaration now looks wrong, that is a
@@ -210,10 +199,13 @@ These are the product, not its limitations.
210
199
 
211
200
  `inkan init` writes a generated protocol block into `AGENTS.md`, the file
212
201
  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`
202
+ seal is a fact; close with dispositions, then commit the record with the
203
+ work and include the outcome trailer; re-anchor with `inkan status` after
204
+ context loss and leave other sessions' outcomes alone; closed outcomes are
205
+ final and commit references are informational when reading history.
206
+ The block states policy only. It names the commands and what each call
207
+ must carry, and leaves flag-level syntax to `inkan help`, so a CLI change
208
+ does not bump the protocol. The block carries a protocol number. `init`
217
209
  upgrades a block it generated under an earlier protocol in place and refuses
218
210
  to overwrite a block that was edited by hand, so the policy lives in exactly
219
211
  one place. `--lang <tag>` sets the language agents should write outcome
@@ -249,29 +241,30 @@ Inkan's own design is recorded this way, from the boundary in `0001`
249
241
  onward. There is no separate design document; `inkan decision list` prints
250
242
  the index.
251
243
 
252
- ## A prototype for the whole lifecycle
244
+ ## One record through the work
253
245
 
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.
246
+ Each stage adds to the record without rewriting an earlier declaration.
258
247
 
259
248
  | Stage | What is recorded | Command |
260
249
  |---|---|---|
261
250
  | Intent | What will be delivered and how it will be judged | `begin` |
262
251
  | Change | How the intent moved, and why | `amend --reason` |
263
252
  | 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` |
253
+ | Close | A disposition per criterion and the status derived from those declarations | `end` |
254
+ | Commit reference | The outcome associated with a landing commit, for context | `Inkan-Outcome` trailer |
267
255
  | Resume | Where a fresh session picks up | `status`, `log` |
268
256
 
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.
257
+ Inkan is built this way itself. Its work is sealed and closed as outcomes,
258
+ and its design decisions live in `.inkan/decisions/`. The repository holds
259
+ the durable context for every agent that works on it.
260
+
261
+ ## Optional file diagnostics
262
+
263
+ `inkan doctor` reports corrupt record files, mismatched or duplicate ids,
264
+ and missing decision references. Run it when you want to diagnose those
265
+ files. It does not judge the work, inspect commits, or repair anything.
266
+ It is not required to close an outcome, commit, or resume a session, and it
267
+ is not part of the generated agent protocol.
275
268
 
276
269
  ## Command reference
277
270
 
@@ -280,11 +273,10 @@ gate in the loop.
280
273
  | `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
274
  | `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
275
  | `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. |
276
+ | `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 outcome id, status, and commit reference trailer. | A live criterion has no disposition, unless closing with `-s abandoned`. No note. |
284
277
  | `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. |
278
+ | `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 and note. Filters combine. | Never. |
279
+ | `inkan doctor` | Optional, read-only diagnostic. 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
280
  | `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
281
  | `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
282
  | `inkan decision list [-s <status>]` / `inkan decision show <id>` | Read-only. `show` accepts `2`, `02`, or `0002`. | Never. |
@@ -309,10 +301,10 @@ outcome is simply a file with no `end` yet.
309
301
 
310
302
  The contract hash is a SHA-256 over the outcome text, its criteria with
311
303
  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.
304
+ and addition. `end` stores that contract hash with the dispositions and
305
+ note. Readers enforce the event format and reject corrupt records; they
306
+ do not inspect project files or compare the record with a commit. Existing
307
+ v1 records that contain Git metadata remain readable without being rewritten.
316
308
 
317
309
  Because each outcome is its own file, two branches never touch the same
318
310
  file and an ordinary merge brings them together. It also keeps review cheap
@@ -322,8 +314,10 @@ benchmark (`npm run bench`) seeds ten thousand closed outcomes and holds
322
314
 
323
315
  ## Status
324
316
 
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
317
+ Inkan began with 0.1.0, rebuilt from scratch as the successor to DriftSeal.
318
+ Current development removes delivery auditing from that first release
319
+ (decision 0015) while retaining outcome trailers as commit references
320
+ (decision 0016). Still deferred: an importer for DriftSeal
327
321
  history and an MCP server. Those are adapters and can follow without
328
322
  changing the record format. The only host-specific convenience is
329
323
  `--claude` on `init` and `skill install`; every other host reads
package/README.zh.md CHANGED
@@ -4,11 +4,9 @@
4
4
 
5
5
  **Agent 许下承诺,Inkan 留下凭据。**
6
6
 
7
- *先把工作真正要交付的结果 seal 下来,再核对最终落地的内容有没有兑现承诺。*
7
+ *先把工作要交付的结果 seal 下来,在结束时如实记录完成情况。*
8
8
 
9
- Inkan 是一款小巧、零依赖的 CLI,专为让 coding agent 真正参与开发的代码仓库而生。它会可靠地记录每项工作原本要交付什么、意图在过程中如何变化,以及工作结束时作出了怎样的声明;再把最终落地的每个 commit 与这份记录绑定。于是,无论人还是 agent,日后都能清楚回答:这个 commit,真的兑现了当初的承诺吗?
10
-
11
- 它同时也是一个更宏大构想的可运行原型:用一份贯穿 agent 工作全生命周期的记录,从意图确定一路走到 commit 审计,并且全程只追加、不改写。
9
+ Inkan 是一款小巧、零依赖的 CLI,面向使用 coding agent 开发的代码仓库。它记录每项工作原本要交付什么、意图在过程中如何变化,以及结束时作出了怎样的声明。记录保存在仓库里,随代码一起提交;换一个 session agent,也能从这里接手工作。
12
10
 
13
11
  ## 问题所在
14
12
 
@@ -16,19 +14,19 @@ Inkan 是一款小巧、零依赖的 CLI,专为让 coding agent 真正参与
16
14
 
17
15
  常见的解决办法是不断增加检查:更多测试、更多 gate,每次查看日志都重新验证一遍。这会让 agent 每次重读历史时,都再次检查自己过去的工作,最终陷入没有尽头的循环。Inkan 选择了另一条路:如实记录当时声明了什么、声明发生在何时,把记录本身视为事实,并把结果是否正确的判断交还给仓库自己的测试体系。
18
16
 
19
- ## 三个问题
20
-
21
- 对于一项工作,Inkan 只回答三个问题,并且刻意不越界。
17
+ ## 记录里有什么
22
18
 
23
19
  | 问题 | Inkan 如何回答 |
24
20
  |---|---|
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 不会替任何人关闭它。 |
21
+ | 工作原本要交付什么? | `begin` outcome 文本和验收标准 seal 到一个 append-only 文件中。`status` 逐字打印 open outcome。 |
22
+ | 意图发生过什么变化? | 标题保留。`amend --reason` 追加原因、补充说明,以及新增或撤回的标准。 |
23
+ | 结束时声明了什么? | `end` 记录每条仍生效标准的 disposition 和结束说明。没有 end event 的 outcome 保持 open |
24
+
25
+ 这些都是工作声明。Inkan 不把它们与 commit 比较,也不判断工作是否满足了标准。关闭之后,没有额外的交付审计步骤,也没有再次证明同一项工作的义务。
28
26
 
29
27
  ## 快速开始
30
28
 
31
- 需要 Node.js 22 或更高版本,以及 git
29
+ 需要 Node.js 22 或更高版本。按仓库平时的 Git 流程提交记录即可;Inkan 本身不调用 Git
32
30
 
33
31
  ```sh
34
32
  npm install --global @rowan-hiro/inkan
@@ -77,44 +75,28 @@ inkan end --met 1 --met 2 --unmet 3 --note "Rate limiting deferred to the next s
77
75
  Inkan-Outcome: 2026-09-03-0621-82qz
78
76
  ```
79
77
 
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
- ```
78
+ 状态根据 disposition 推导:所有标准都声明为 met 时是 `completed`,只要有一项声明为 unmet 就是 `partial`。如实记录的 `partial` 是一种完整、正式的结果;agent 应当报告它,而不是为了声称“完成”而拉伸 done 的定义。
87
79
 
88
- trailer 必须放在 commit message 的最后一个段落,并与 `Co-Authored-By` 等其他 trailer 相邻,中间不能有空行。Git 只会把最后一个段落中的内容识别为 trailer;用空行单独隔开的 trailer,不会报错,却也不会被识别。
80
+ **5. 把工作和记录一起提交。**
89
81
 
90
- **6. 日后随时检查:这个 commit 是否兑现了承诺?**
82
+ 把本项工作涉及的文件加入暂存区,包括 `.inkan/` 中的记录。落地 commit 要带上 `end` 打印的 trailer:
91
83
 
92
84
  ```sh
93
- inkan check HEAD
85
+ git commit -m "feat: account recovery" \
86
+ -m "Inkan-Outcome: 2026-09-03-0621-82qz"
94
87
  ```
95
88
 
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
- ```
89
+ trailer 放在 commit message 的最后一段,与其他 trailer 相邻,中间不留空行。agent protocol 要求提交时写上这条关联信息;Inkan 不安装 hook,也不通过程序阻塞提交来执行这项规则。
103
90
 
104
- ## 偷换 outcome 会是什么样
91
+ outcome 已经关闭,后面没有需要执行的交付审计。
105
92
 
106
- 假设 outcome 关闭后,代码又被修改,却仍使用同一个 trailer 提交。此时,记录中的 tree 将不再匹配这个 commit:
93
+ ## 阅读历史时使用关联信息
107
94
 
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
- ```
95
+ `Inkan-Outcome: <id>` 把 commit 与 outcome 联系起来,方便了解工作原本要交付什么、过程中如何调整,以及结束时声明了什么。它不证明工作已完成、验收条件已满足,也不证明 commit 的内容。
116
96
 
117
- 一致时退出码为 0,不匹配时为 1;如果 commit 完全没有 `Inkan-Outcome` trailer,则为 2。Inkan 不会修复任何内容,也不会阻塞任何操作。`check` 只是对过去作出报告——也正因为如此,它可以安心地运行在 code review 或 CI job 中,而不会摇身变成一道 gate。
97
+ 需要这些上下文时再沿着引用读取。了解历史时,优先通过普通 Git 命令读取该 commit 中的 `.inkan/outcomes/<id>.jsonl`;`inkan log <id>` 读取的是当前 checkout 中的记录。这种读取只提供上下文,不作一致性判断。
98
+
99
+ 缺少 trailer 或找不到关联记录,只表示信息缺失。阅读历史不要求验证交付、补填 trailer、修复旧 commit 或重新打开已关闭的 outcome。提交规则用于写入新的 commit,不给阅读者增加追溯修补的义务。
118
100
 
119
101
  ## Context 丢失之后
120
102
 
@@ -148,8 +130,8 @@ inkan log -n 3
148
130
 
149
131
  这些不是功能缺失,而是产品本身的边界。
150
132
 
151
- - **它绝不运行任何任务。** 不运行测试、build 或 shell command。Inkan 唯一会启动的 child process 是 git,而且参数列表固定。工作是否正确,应由仓库自己判断。
152
- - **它绝不充当 gate。** `git commit` 之前或期间不会运行任何 Inkan 操作,`init` 也不会安装 hook。`check` `doctor` 只报告已经存在的 commit 和文件。
133
+ - **它绝不运行任何任务。** 不运行测试、build 或 shell command。Inkan 不启动任何 child process,包括 Git。工作是否正确,应由仓库自己判断。
134
+ - **它绝不充当 gate。** `git commit` 之前或期间不会运行任何 Inkan 操作,`init` 也不会安装 hook。没有 commit 比较或交付审计。`doctor` 是可选的文件诊断工具,不是工作流程中的必经步骤。
153
135
  - **关闭即最终状态。** 没有 stale state,没有 invalidation,也不存在已经关闭的 outcome 还需要重做的概念。查看日志就是阅读,而不是重新检查。如果过去的声明如今看来有误,那应当成为一项拥有独立 seal 的新 outcome。
154
136
  - **它绝不代替别人关闭 outcome。** 多个 outcome 可以同时 open,每个 session 或 branch 各自拥有一个。`begin` 会指出其他 outcome 的存在,但不会碰它们。从未关闭的 outcome,就是“它确实没有关闭”的诚实记录;为何一直 open,应由人来调查,而不是由 agent 擅自判断。仅仅为了关闭而关闭,只会让日志充斥无意义的记录。
155
137
  - **它绝不改写当时的场景。** 情况变化时,agent 可以通过 amendment 或新的 decision record 对既有决定提出挑战,但绝不会修改那段记录了当时所知信息与所作决定的文本。
@@ -157,7 +139,7 @@ inkan log -n 3
157
139
 
158
140
  ## 为 agent 而生
159
141
 
160
- `inkan init` 会把生成好的 protocol block 写进 coding agent 本来就会读取的 `AGENTS.md`。其中只有五条规则:在 durable change 之前 seal;seal 是事实;先逐项 disposition 并关闭,再把 trailer 放在 commit message 的最后一段完成提交;context 丢失后用 `inkan status` 重新锚定,同时不碰其他 session 的 outcome;关闭即最终状态。
142
+ `inkan init` 会把生成好的 protocol block 写进 coding agent 本来就会读取的 `AGENTS.md`。其中只有五条规则:在 durable change 之前 seal;seal 是事实;先逐项 disposition 并关闭,再把记录与工作一起提交,并写入 outcome trailercontext 丢失后用 `inkan status` 重新锚定,同时不碰其他 session 的 outcome;关闭即最终状态,阅读历史时仅把 commit 引用作为辅助信息。
161
143
 
162
144
  protocol block 带有版本号。`init` 会原地升级由旧版 protocol 生成的 block,但拒绝覆盖经过手工编辑的 block,确保 policy 始终只有一个权威来源。`--lang <tag>` 用来设置 agent 撰写 outcome 文本时应使用的语言。`inkan init --claude` 还会把 `CLAUDE.md` 创建为指向 `AGENTS.md` 的 symlink:Claude Code 读的是自己认识的文件名,而 policy 依然只有一份,不是副本。
163
145
 
@@ -179,23 +161,26 @@ outcome 可以在 `begin` 或 `amend` 时通过 `--decision <id>` 指明自己
179
161
 
180
162
  Inkan 自己的设计也用同样的方式记录,从 `0001` 中划定的边界一路延续至今。仓库没有单独的设计文档;`inkan decision list` 就是它的索引。
181
163
 
182
- ## 面向完整生命周期的原型
164
+ ## 工作共用一份记录
183
165
 
184
- Inkan 还是一个更大构想的可运行原型:管理 agent 工作的完整生命周期,从意图确定的那一刻,到 commit 接受审计的那一刻。整个过程中,每个阶段都向同一份记录写入事实,没有任何阶段会改写过去。
166
+ 每个阶段都向记录追加内容,不改写此前的声明。
185
167
 
186
168
  | 阶段 | 记录什么 | 命令 |
187
169
  |---|---|---|
188
170
  | 意图 | 要交付什么,以及如何判断是否达成 | `begin` |
189
171
  | 变更 | 意图如何变化,以及为什么变化 | `amend --reason` |
190
172
  | 约束 | 当前工作受哪些 decision 约束 | `decision add`、`--decision` |
191
- | 关闭 | 每条标准的 disposition,以及由此推导出的诚实状态 | `end` |
192
- | 交付 | 真正落地的 commit,并与 seal 绑定 | `Inkan-Outcome` trailer |
193
- | 审计 | commit 与记录是否仍然一致 | `check`、`doctor` |
173
+ | 关闭 | 每条标准的 disposition,以及由这些声明推导出的状态 | `end` |
174
+ | 提交关联 | commit 关联的 outcome,供阅读时了解上下文 | `Inkan-Outcome` trailer |
194
175
  | 恢复 | 新 session 从哪里接手 | `status`、`log` |
195
176
 
196
- Inkan 本身就是这样构建的。仓库从第二个 commit 开始,每个 commit 都带有 `Inkan-Outcome` trailer;每个 milestone 都先作为 outcome 被 seal,随后关闭;所有设计决定都保存在 `.inkan/decisions/` 中。
177
+ Inkan 本身也这样开发:工作先作为 outcome 被 seal,结束时记录声明;设计决定保存在 `.inkan/decisions/` 中。仓库保存长期上下文,供参与工作的每个 agent 使用。
178
+
179
+ ## 可选的文件诊断
180
+
181
+ `inkan doctor` 报告记录文件损坏、id 不匹配或重复,以及缺失的 decision 引用。需要排查这些文件时可以主动运行;它不判断工作结果、不读取 commit,也不修复内容。
197
182
 
198
- 这份记录被刻意设计得很小。这个原型真正要验证的是:不在流程里加入 runner,也不设置 gate,仅凭一份只记录事实的日志,能否管理完整的生命周期。
183
+ 关闭 outcome、提交代码和恢复 session 都不要求运行 `doctor`,生成的 agent protocol 也不包含这一步。
199
184
 
200
185
  ## 命令参考
201
186
 
@@ -204,11 +189,10 @@ Inkan 本身就是这样构建的。仓库从第二个 commit 开始,每个 co
204
189
  | `inkan init [--lang <tag>] [--claude]` | 写入或升级 `AGENTS.md` 中由 Inkan 管理的 block;创建 `.inkan/`。`--claude` 还会把 `CLAUDE.md` 软链到 `AGENTS.md`。 | block 曾被手工编辑;已存在一个不是该 symlink 的 `CLAUDE.md`。 |
205
190
  | `inkan begin "<outcome>" [--accept <text>]... [--decision <id>]... [--lane <tag>]` | Seal 一个新 outcome,并打印其 id。其他 open outcome 会在 stderr 的 notice 中被点名,但不会受到任何改动。 | 永不拒绝。 |
206
191
  | `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。 |
192
+ | `inkan end [<id>] [--met <n>]... [--unmet <n>]... [-s abandoned] --note <text>` | 记录 disposition 并关闭 outcome。状态由结果推导:全部 met 为 `completed`,任一 unmet 为 `partial`。打印 outcome id、状态和供 commit 使用的关联 trailer | 仍生效的标准缺少 disposition(以 `-s abandoned` 关闭时除外);没有 note。 |
208
193
  | `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。 | 永不拒绝。 |
194
+ | `inkan log [-n N] [--since <date>] [--grep <regex>] [--status <s>] [--decision <id>] [--lane <tag>] [<id>]` | 每个 outcome 打印一行,最新的在前,默认 20 条。`<id>` 会完整打印一项 outcome,包括 disposition note。filter 可以组合。 | 永不拒绝。 |
195
+ | `inkan doctor` | 可选的只读文件诊断。Fold 所有 outcome 并解析所有 decision;报告损坏文件、id 不匹配、重复的 decision id,以及失效的 decision link。退出码:正常为 0,发现问题为 1。 | 永不拒绝。 |
212
196
  | `inkan decision add "<title>" --context <text> --decision <text> [--driver <text>]... [--option <text>]... [--consequence <text>]... [-s <status>]` | 写入一个带编号的 MADR 文件,并打印其路径。 | 缺少必要 section。 |
213
197
  | `inkan decision update <id> --status <status> --reason <text>` | 追加一条带日期的历史记录,并设置新状态。有 open outcome 时会指出它的名称。永不编辑 Context 或 Decision Outcome。 | id 或 status 未知。 |
214
198
  | `inkan decision list [-s <status>]` / `inkan decision show <id>` | 只读。`show` 接受 `2`、`02` 或 `0002`。 | 永不拒绝。 |
@@ -226,13 +210,13 @@ Decision status 包括 `proposed`、`accepted`、`rejected`、`deferred`、`depr
226
210
 
227
211
  `2026-09-03-1432-k7m2` 这样的 outcome id,由 outcome 开始时的 UTC 日期与分钟,加上四个随机字符组成。因此 id 可以按时间排序,两个 branch 也几乎不可能发生冲突。每个 outcome 文件包含一个 `begin` event、任意数量的 `amend` event,以及最多一个 `end` event。所谓 open outcome,就是一个尚无 `end` 的文件。
228
212
 
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,并同时比较两者。
213
+ contract hash 是一个 SHA-256,计算范围包括 outcome 文本、带 withdrawn 标记的验收标准、关联的 decision,以及每次 amendment 的 reason 和 addition。`end` 把这个 hash disposition note 一起保存。读取时仍会检查 event 格式并拒绝损坏的记录,但不会读取项目文件或与 commit 比较。旧 v1 记录中的 Git 信息可以继续读取,不会被改写。
230
214
 
231
215
  由于每个 outcome 都有独立文件,两个 branch 永远不会改动同一个 outcome 文件,普通 merge 就能把记录自然汇合。这种设计不需要 cache,也能让 review 保持轻快:不带 filter 的 `log` 只读取实际要打印的文件数;内置 benchmark(`npm run bench`)会生成一万个已关闭的 outcome,并将 `log -n 3` 控制在 50 ms 以内、`log --grep` 控制在 1 秒以内、`doctor` 控制在 2 秒以内。
232
216
 
233
217
  ## 当前状态
234
218
 
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。
219
+ Inkan 0.1.0 起作为 DriftSeal 的继任者,从零重新构建。当前开发版本移除了首发版本中的交付审计(decision 0015),同时保留 outcome trailer 作为 commit 的关联信息(decision 0016)。目前仍未加入 DriftSeal 历史记录 importer 和 MCP server。这些都属于 adapter,可以后续补上,而无需改变记录格式。唯一针对特定 host 的便利是 `init` 和 `skill install` 的 `--claude`;其他 host 直接读取 `AGENTS.md` 和 `.agents/skills`,无需任何适配。Lane 目前只作为 `begin` 时可选的归档 tag,以及 `log` 的 filter。
236
220
 
237
221
  ## License
238
222
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowan-hiro/inkan",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Seal the authoritative outcome; keep a trustworthy record of the process.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -8,7 +8,6 @@ import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import * as store from './store.js';
10
10
  import { fold, computeContractHash } from './fold.js';
11
- import * as git from './git.js';
12
11
  import * as decisions from './decisions.js';
13
12
 
14
13
  export class InkanError extends Error {
@@ -116,7 +115,6 @@ export function begin({ root, outcome, accept = [], decision = [], lane }) {
116
115
  // Other open outcomes belong to whoever began them. They are reported,
117
116
  // never closed or otherwise touched here (decision 0013).
118
117
  const openAlongside = openRecords(resolvedRoot).map((r) => ({ id: r.id, outcome: r.outcome }));
119
- const head = git.head(resolvedRoot);
120
118
  const existingIds = store.listOutcomeIds(resolvedRoot);
121
119
  const resolvedLane = lane ?? null;
122
120
 
@@ -131,11 +129,10 @@ export function begin({ root, outcome, accept = [], decision = [], lane }) {
131
129
  criteria: accept,
132
130
  decisions: decision,
133
131
  lane: resolvedLane,
134
- head,
135
132
  };
136
133
  try {
137
134
  store.createOutcomeFile(resolvedRoot, id, event);
138
- return { id, outcome, criteria: accept, decisions: decision, lane: resolvedLane, head, openAlongside };
135
+ return { id, outcome, criteria: accept, decisions: decision, lane: resolvedLane, openAlongside };
139
136
  } catch (err) {
140
137
  existingIds.push(id);
141
138
  if (attempt === 4) throw err;
@@ -159,7 +156,6 @@ export function amend({ root, id, reason, addition, accept = [], withdraw = [],
159
156
  const criterion = record.criteria[n - 1];
160
157
  if (!criterion || criterion.withdrawn) throw new InkanError(`cannot withdraw unknown or already-withdrawn criterion ${n}`);
161
158
  }
162
- const head = git.head(resolvedRoot);
163
159
  store.appendEvent(resolvedRoot, record.id, {
164
160
  v: 1,
165
161
  type: 'amend',
@@ -170,7 +166,6 @@ export function amend({ root, id, reason, addition, accept = [], withdraw = [],
170
166
  criteria: accept,
171
167
  withdraw: withdrawIndexes,
172
168
  decisions: decision,
173
- head,
174
169
  });
175
170
  const updated = loadRecord(resolvedRoot, record.id);
176
171
  return { id: record.id, contractHash: computeContractHash(updated) };
@@ -213,8 +208,6 @@ export function end({ root, id, met = [], unmet = [], status, note }) {
213
208
  finalStatus = dispositions.some((d) => !d.met) ? 'partial' : 'completed';
214
209
  }
215
210
  const contractHash = computeContractHash(record);
216
- const tree = git.treeHash(resolvedRoot);
217
- const head = git.head(resolvedRoot);
218
211
  store.appendEvent(resolvedRoot, record.id, {
219
212
  v: 1,
220
213
  type: 'end',
@@ -224,8 +217,6 @@ export function end({ root, id, met = [], unmet = [], status, note }) {
224
217
  dispositions,
225
218
  note,
226
219
  contractHash,
227
- tree,
228
- head,
229
220
  });
230
221
 
231
222
  return { id: record.id, status: finalStatus };
@@ -296,61 +287,9 @@ export function log({ root, n, lane, since, grep, status, decision, id }) {
296
287
  return { records: records.slice(0, limit) };
297
288
  }
298
289
 
299
- // --- check / doctor ---------------------------------------------------------
290
+ // --- doctor ----------------------------------------------------------------
300
291
 
301
- const OUTCOME_FILE_PREFIX = '.inkan/outcomes/';
302
-
303
- /** The four facts for one `Inkan-Outcome` trailer value, per decision 0006. */
304
- function checkTrailer(root, sha, id) {
305
- const filePath = `${OUTCOME_FILE_PREFIX}${id}.jsonl`;
306
- const raw = git.showFile(root, sha, filePath);
307
- if (raw === null) return { id, lines: ['outcome: missing from commit'], ok: false };
308
-
309
- let events;
310
- try {
311
- events = store.parseOutcomeEvents(raw, `${sha}:${filePath}`);
312
- } catch (err) {
313
- const firstLine = String(err.message).split('\n')[0];
314
- return { id, lines: [`outcome: present, unreadable (${firstLine})`], ok: false };
315
- }
316
- const endEvent = events.find((e) => e && e.type === 'end');
317
- if (!endEvent) return { id, lines: ['outcome: present, open'], ok: false };
318
-
319
- let hashOk = true;
320
- try {
321
- fold(events, `${sha}:${filePath}`);
322
- } catch {
323
- hashOk = false;
324
- }
325
- const lines = [`outcome: present, closed (${endEvent.status})`, hashOk ? 'hash: matches refold' : 'hash: does not match refold'];
326
-
327
- let treeOk = true;
328
- if (endEvent.tree == null) {
329
- lines.push('tree: not recorded');
330
- } else if (git.treeMatchesCommit(root, endEvent.tree, sha)) {
331
- lines.push('tree: matches commit tree');
332
- } else {
333
- lines.push('tree: differs from commit tree');
334
- treeOk = false;
335
- }
336
-
337
- return { id, lines, ok: hashOk && treeOk };
338
- }
339
-
340
- /** Read-only report on whether a commit's `Inkan-Outcome` trailers stay faithful to what it recorded. */
341
- export function check({ root, commit }) {
342
- const resolvedRoot = resolveRoot(root);
343
- const ref = commit ?? 'HEAD';
344
- const sha = git.revParse(resolvedRoot, ref);
345
- if (!sha) throw new InkanError(`unknown commit "${ref}"`);
346
- const shortSha = git.shortSha(resolvedRoot, sha);
347
- const trailerIds = git.trailerValues(resolvedRoot, sha);
348
- if (trailerIds.length === 0) return { shortSha, noTrailer: true };
349
- const reports = trailerIds.map((id) => checkTrailer(resolvedRoot, sha, id));
350
- return { shortSha, reports, consistent: reports.every((r) => r.ok) };
351
- }
352
-
353
- /** Read-only report: folds every outcome, parses every decision, and cross-checks ids and links. */
292
+ /** Optional file diagnostic: folds outcomes, parses decisions, and reports id/link problems. */
354
293
  export function doctor({ root }) {
355
294
  const resolvedRoot = resolveRoot(root);
356
295
  const problems = [];
@@ -519,6 +458,7 @@ Outcome log: \`.inkan/outcomes/<id>.jsonl\`, one append-only file per outcome. C
519
458
  ${END_MARKER}`;
520
459
  }
521
460
 
461
+ // Protocol 4, frozen verbatim for upgrades. Never edit.
522
462
  function protocolBlockV4(lang) {
523
463
  return `${START_MARKER}
524
464
  <!-- inkan-protocol: 4 -->
@@ -540,7 +480,71 @@ Outcome log: \`.inkan/outcomes/<id>.jsonl\`, one append-only file per outcome. C
540
480
  ${END_MARKER}`;
541
481
  }
542
482
 
543
- const PROTOCOL_VERSION = 4;
483
+ // Protocol 5, frozen verbatim for upgrades. Never edit.
484
+ function protocolBlockV5(lang) {
485
+ return `${START_MARKER}
486
+ <!-- inkan-protocol: 5 -->
487
+ <!-- inkan-lang: ${lang} -->
488
+
489
+ ## Agent protocol: sealed outcomes
490
+
491
+ This repository uses Inkan (\`inkan\`, alias \`ink\`). Inkan keeps a trustworthy record of what the work was meant to deliver and what was declared at close. It does not inspect commits, run tests, or judge the result; the repository's own checks do that. Write outcome prose in ${lang}.
492
+
493
+ 1. **Seal before durable changes.** Before changing code, configuration, documentation, or dependencies, run \`inkan status\`; if it shows an open outcome that is not your work, follow rule 4 first. Then run \`inkan begin "<outcome>" --accept "<observable criterion>"\`. Repeat \`--accept\` per criterion. Add \`--decision <id>\` for each decision record this work is bound by. Add \`--lane <tag>\` only when the repository already files outcomes by lane.
494
+ 2. **The seal is a fact.** Deliver what it says. If circumstances change, do not reinterpret it: run \`inkan amend --reason "<what changed>"\` with the added or withdrawn criteria. The original text stays. Never question why the outcome was sealed the way it was at the time.
495
+ 3. **Close with dispositions, then commit.** Run \`inkan end --met <n>... [--unmet <n>...] --note "<what happened>"\`. Every live criterion gets a disposition. Commit the outcome record with the work. Never report success without closing the outcome.
496
+ 4. **Re-anchor after context loss.** Run \`inkan status\` and \`inkan log -n 3\`. An open outcome that is the work you were asked to do is your task: continue it, or close it with a note. An open outcome that is not your work belongs to another session: leave it alone. Never close, amend, or abandon an outcome you did not work on, and do not judge why it is still open. Before beginning your own outcome beside it, stop and tell the person it is there, and ask whether your work should run in its own git worktree, because separate worktrees keep each session's edits apart.
497
+ 5. **Closed outcomes are final.** Reviewing the log is reading, not re-checking. Never re-verify, re-attest, or re-close a closed outcome. If a past declaration now looks wrong, that is a new outcome with its own seal.
498
+
499
+ Decision records live in \`.inkan/decisions/\`. Their Context and Decision sections record the scenario at the time and are never edited. To challenge one, run \`inkan decision update <id> --status <status> --reason "<what changed>"\` or add a new record that supersedes it.
500
+
501
+ Outcome log: \`.inkan/outcomes/<id>.jsonl\`, one append-only file per outcome. Commit \`.inkan/\` with the code. Do not edit these files by hand.
502
+ ${END_MARKER}`;
503
+ }
504
+
505
+ function protocolBlockV6(lang) {
506
+ return `${START_MARKER}
507
+ <!-- inkan-protocol: 6 -->
508
+ <!-- inkan-lang: ${lang} -->
509
+
510
+ ## Agent protocol: sealed outcomes
511
+
512
+ This repository uses Inkan (\`inkan\`, alias \`ink\`). Inkan keeps a trustworthy record of what the work was meant to deliver and what was declared at close. It does not inspect commits, run tests, or judge the result; the repository's own checks do that. Write outcome prose in ${lang}.
513
+
514
+ 1. **Seal before durable changes.** Before changing code, configuration, documentation, or dependencies, run \`inkan status\`; if it shows an open outcome that is not your work, follow rule 4 first. Then run \`inkan begin "<outcome>" --accept "<observable criterion>"\`. Repeat \`--accept\` per criterion. Add \`--decision <id>\` for each decision record this work is bound by. Add \`--lane <tag>\` only when the repository already files outcomes by lane.
515
+ 2. **The seal is a fact.** Deliver what it says. If circumstances change, do not reinterpret it: run \`inkan amend --reason "<what changed>"\` with the added or withdrawn criteria. The original text stays. Never question why the outcome was sealed the way it was at the time.
516
+ 3. **Close with dispositions, then commit.** Run \`inkan end --met <n>... [--unmet <n>...] --note "<what happened>"\`. Every live criterion gets a disposition. Commit the outcome record with the work. Include the printed \`Inkan-Outcome: <id>\` trailer in the final paragraph of the landing commit message, beside any other trailers with no blank line between them. Never report success without closing the outcome.
517
+ 4. **Re-anchor after context loss.** Run \`inkan status\` and \`inkan log -n 3\`. An open outcome that is the work you were asked to do is your task: continue it, or close it with a note. An open outcome that is not your work belongs to another session: leave it alone. Never close, amend, or abandon an outcome you did not work on, and do not judge why it is still open. Before beginning your own outcome beside it, stop and tell the person it is there, and ask whether your work should run in its own git worktree, because separate worktrees keep each session's edits apart.
518
+ 5. **Closed outcomes are final.** Reviewing the log is reading, not re-checking. Never re-verify, re-attest, or re-close a closed outcome. If a past declaration now looks wrong, that is a new outcome with its own seal. When reading history, use commit trailers only as references. Missing trailers or unavailable referenced records are missing information, not failed outcomes or a reason to verify delivery or repair history.
519
+
520
+ Decision records live in \`.inkan/decisions/\`. Their Context and Decision sections record the scenario at the time and are never edited. To challenge one, run \`inkan decision update <id> --status <status> --reason "<what changed>"\` or add a new record that supersedes it.
521
+
522
+ Outcome log: \`.inkan/outcomes/<id>.jsonl\`, one append-only file per outcome. Commit \`.inkan/\` with the code. Do not edit these files by hand.
523
+ ${END_MARKER}`;
524
+ }
525
+
526
+ function protocolBlockV7(lang) {
527
+ return `${START_MARKER}
528
+ <!-- inkan-protocol: 7 -->
529
+ <!-- inkan-lang: ${lang} -->
530
+
531
+ ## Agent protocol: sealed outcomes
532
+
533
+ This repository uses Inkan (\`inkan\`, alias \`ink\`). Inkan keeps a trustworthy record of what the work was meant to deliver and what was declared at close. It does not inspect commits, run tests, or judge the result; the repository's own checks do that. Write outcome prose in ${lang}. This block states the policy; \`inkan help\` gives the command syntax.
534
+
535
+ 1. **Seal before durable changes.** Before changing code, configuration, documentation, or dependencies, run \`inkan status\`; if it shows an open outcome that is not your work, follow rule 4 first. Then run \`inkan begin\` with the outcome, one observable acceptance criterion at a time, and every decision record the work is bound by. File the outcome by lane only when the repository already files outcomes by lane.
536
+ 2. **The seal is a fact.** Deliver what it says. If circumstances change, do not reinterpret it: run \`inkan amend\` with the reason and the added or withdrawn criteria. The original text stays. Never question why the outcome was sealed the way it was at the time.
537
+ 3. **Close with dispositions, then commit.** Run \`inkan end\` with a disposition, met or unmet, for every live criterion and a note on what happened. Commit the outcome record with the work. Include the printed \`Inkan-Outcome: <id>\` trailer in the final paragraph of the landing commit message, beside any other trailers with no blank line between them. Never report success without closing the outcome.
538
+ 4. **Re-anchor after context loss.** Run \`inkan status\` and \`inkan log -n 3\`. An open outcome that is the work you were asked to do is your task: continue it, or close it with a note. An open outcome that is not your work belongs to another session: leave it alone. Never close, amend, or abandon an outcome you did not work on, and do not judge why it is still open. Before beginning your own outcome beside it, stop and tell the person it is there, and ask whether your work should run in its own git worktree, because separate worktrees keep each session's edits apart.
539
+ 5. **Closed outcomes are final.** Reviewing the log is reading, not re-checking. Never re-verify, re-attest, or re-close a closed outcome. If a past declaration now looks wrong, that is a new outcome with its own seal. When reading history, use commit trailers only as references. Missing trailers or unavailable referenced records are missing information, not failed outcomes or a reason to verify delivery or repair history.
540
+
541
+ Decision records live in \`.inkan/decisions/\`. Their Context and Decision sections record the scenario at the time and are never edited. To challenge one, run \`inkan decision update\` with the new status and the reason, or add a new record that supersedes it.
542
+
543
+ Outcome log: \`.inkan/outcomes/<id>.jsonl\`, one append-only file per outcome. Commit \`.inkan/\` with the code. Do not edit these files by hand.
544
+ ${END_MARKER}`;
545
+ }
546
+
547
+ const PROTOCOL_VERSION = 7;
544
548
 
545
549
  /**
546
550
  * The managed block for `lang` at protocol `version`, current by default.
@@ -552,6 +556,9 @@ export function protocolBlock(lang, version = PROTOCOL_VERSION) {
552
556
  if (version === 2) return protocolBlockV2(lang);
553
557
  if (version === 3) return protocolBlockV3(lang);
554
558
  if (version === 4) return protocolBlockV4(lang);
559
+ if (version === 5) return protocolBlockV5(lang);
560
+ if (version === 6) return protocolBlockV6(lang);
561
+ if (version === 7) return protocolBlockV7(lang);
555
562
  throw new InkanError(`unknown protocol version ${version}`);
556
563
  }
557
564
 
package/src/cli.js CHANGED
@@ -22,24 +22,30 @@ Commands:
22
22
  Create .inkan/ and write the agent protocol block into AGENTS.md.
23
23
  --claude also links CLAUDE.md to AGENTS.md.
24
24
  begin "<outcome>" [--accept <text>]... [--decision <id>]... [--lane <tag>]
25
- Seal a new outcome; prints its id.
25
+ Seal a new outcome; prints its id. Repeat --accept once per
26
+ observable criterion; they are numbered from 1 in that order.
27
+ Repeat --decision once per decision record the work is bound by.
28
+ Use --lane only where the repository already files outcomes by lane.
26
29
  amend --reason <text> [<addition>] [--accept <text>]... [--withdraw <n>]...
27
30
  [--decision <id>]... [<id>]
28
31
  Append an amendment to the open outcome; prints the new contract hash.
32
+ --reason is required. Added criteria continue the numbering;
33
+ --withdraw takes a criterion number. The original text is kept.
29
34
  end [<id>] [--met <n>]... [--unmet <n>]... [-s abandoned] --note <text>
30
- Record dispositions and close an outcome.
35
+ Record dispositions and close an outcome; prints the Inkan-Outcome
36
+ trailer to put in the landing commit. Repeat --met or --unmet once
37
+ per live criterion number; every live criterion needs one. A value
38
+ may carry a note as "<n>: <text>". --note is required.
31
39
  status
32
40
  Print every open outcome.
33
41
  log [-n <count>] [--since <date>] [--grep <regex>] [--status <s>]
34
42
  [--decision <id>] [--lane <tag>] [<id>]
35
43
  Print the outcome log, newest first; <id> prints one outcome in full.
36
44
  Filters combine.
37
- check [<commit>]
38
- Read-only. Reports whether a commit's Inkan-Outcome trailers still
39
- match what was recorded. Exit 0 consistent, 1 mismatch, 2 no trailer.
40
45
  doctor
41
- Read-only. Reports corrupt outcomes, id mismatches, duplicate
42
- decision ids, and dangling decision links. Exit 0 clean, 1 problems.
46
+ Optional, read-only file diagnostic. Reports corrupt outcomes, id
47
+ mismatches, duplicate decision ids, and dangling decision links.
48
+ Exit 0 clean, 1 problems. Never required to close an outcome.
43
49
  decision add "<title>" --context <text> --decision <text> [--driver <text>]...
44
50
  [--option <text>]... [--consequence <text>]... [-s <status>]
45
51
  Write a numbered MADR record; prints its file path.
@@ -114,8 +120,6 @@ function printRecord(record) {
114
120
  console.log(` closed: ${record.closedAt}`);
115
121
  console.log(` status: ${record.status}`);
116
122
  console.log(` note: ${record.note}`);
117
- console.log(` tree: ${record.tree ?? 'none'}`);
118
- console.log(` head: ${record.head ?? 'none'}`);
119
123
  }
120
124
  }
121
125
 
@@ -127,25 +131,6 @@ function printStatus(open) {
127
131
  for (const record of open) printRecord(record);
128
132
  }
129
133
 
130
- function printCheck(result) {
131
- if (result.noTrailer) {
132
- console.log(`${result.shortSha} no Inkan-Outcome trailer`);
133
- process.exitCode = 2;
134
- return;
135
- }
136
- for (const r of result.reports) {
137
- console.log(`${result.shortSha} Inkan-Outcome: ${r.id}`);
138
- for (const line of r.lines) console.log(` ${line}`);
139
- }
140
- if (result.consistent) {
141
- console.log('consistent');
142
- } else {
143
- console.log('mismatch');
144
- console.log('a mismatch is a fact about this commit; it is recorded, not repaired');
145
- process.exitCode = 1;
146
- }
147
- }
148
-
149
134
  function printDoctor(result) {
150
135
  if (result.problems.length === 0) {
151
136
  console.log(`ok: ${result.outcomeCount} outcomes, ${result.decisionCount} decisions`);
@@ -315,12 +300,6 @@ function run(argv) {
315
300
  else for (const record of result.records) printLogLine(record);
316
301
  break;
317
302
  }
318
- case 'check': {
319
- const { positionals } = parseArgs({ args: rest, allowPositionals: true });
320
- if (positionals.length > 1) throw new InkanError('usage: inkan check [<commit>]');
321
- printCheck(api.check({ root, commit: positionals[0] }));
322
- break;
323
- }
324
303
  case 'doctor': {
325
304
  parseArgs({ args: rest });
326
305
  printDoctor(api.doctor({ root }));
package/src/git.js DELETED
@@ -1,83 +0,0 @@
1
- // The only child process Inkan ever spawns: git, with a fixed argument
2
- // array. Never `shell: true`, never a user-supplied command string.
3
-
4
- import { spawnSync } from 'node:child_process';
5
- import fs from 'node:fs';
6
- import os from 'node:os';
7
- import path from 'node:path';
8
- import crypto from 'node:crypto';
9
-
10
- function run(args, { cwd, env }) {
11
- return spawnSync('git', args, { cwd, env, encoding: 'utf8' });
12
- }
13
-
14
- /** The full sha `ref` resolves to, or null if it does not resolve here. */
15
- export function revParse(cwd, ref) {
16
- const result = run(['rev-parse', ref], { cwd, env: process.env });
17
- if (result.status !== 0) return null;
18
- return result.stdout.trim();
19
- }
20
-
21
- /** The HEAD sha, or null outside a worktree or before the first commit. */
22
- export function head(cwd) {
23
- return revParse(cwd, 'HEAD');
24
- }
25
-
26
- export function isWorktree(cwd) {
27
- const result = run(['rev-parse', '--is-inside-work-tree'], { cwd, env: process.env });
28
- return result.status === 0 && result.stdout.trim() === 'true';
29
- }
30
-
31
- /** The abbreviated form of `sha`, or `sha` itself if git cannot shorten it. */
32
- export function shortSha(cwd, sha) {
33
- const result = run(['rev-parse', '--short', sha], { cwd, env: process.env });
34
- return result.status === 0 ? result.stdout.trim() : sha;
35
- }
36
-
37
- /** Values of `commit`'s `Inkan-Outcome` trailers, in order; empty if none or if `commit` does not resolve. */
38
- export function trailerValues(cwd, commit) {
39
- const result = run(['log', '-1', '--format=%(trailers:key=Inkan-Outcome,valueonly)', commit], { cwd, env: process.env });
40
- if (result.status !== 0) return [];
41
- return result.stdout.split('\n').filter((line) => line.length > 0);
42
- }
43
-
44
- /** The content of `filePath` as it exists in `commit`'s tree, or null if it is not there. */
45
- export function showFile(cwd, commit, filePath) {
46
- const result = run(['show', `${commit}:${filePath}`], { cwd, env: process.env });
47
- return result.status === 0 ? result.stdout : null;
48
- }
49
-
50
- /** Whether `recordedTree` matches `commit`'s tree, `.inkan/outcomes` excluded. */
51
- export function treeMatchesCommit(cwd, recordedTree, commit) {
52
- const result = run(
53
- ['diff-tree', '-r', '--quiet', recordedTree, `${commit}^{tree}`, '--', '.', ':(exclude).inkan/outcomes'],
54
- { cwd, env: process.env }
55
- );
56
- return result.status === 0;
57
- }
58
-
59
- /**
60
- * The tree hash of the working tree, `.inkan/outcomes` excluded, via the
61
- * temporary-index recipe from decision 0006. Returns null outside a worktree.
62
- * The temporary index file is always cleaned up.
63
- */
64
- export function treeHash(cwd) {
65
- if (!isWorktree(cwd)) return null;
66
- const tmp = path.join(os.tmpdir(), `inkan-index-${process.pid}-${crypto.randomUUID()}`);
67
- const env = { ...process.env, GIT_INDEX_FILE: tmp };
68
- try {
69
- let result = run(['read-tree', '--empty'], { cwd, env });
70
- if (result.status !== 0) throw new Error(`git read-tree failed: ${result.stderr}`);
71
- result = run(['add', '-A', '--', '.', ':(exclude).inkan/outcomes'], { cwd, env });
72
- if (result.status !== 0) throw new Error(`git add failed: ${result.stderr}`);
73
- result = run(['write-tree'], { cwd, env });
74
- if (result.status !== 0) throw new Error(`git write-tree failed: ${result.stderr}`);
75
- return result.stdout.trim();
76
- } finally {
77
- try {
78
- fs.unlinkSync(tmp);
79
- } catch {
80
- // already gone
81
- }
82
- }
83
- }