@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 +79 -85
- package/README.zh.md +39 -55
- package/package.json +1 -1
- package/src/api.js +72 -65
- package/src/cli.js +13 -34
- package/src/git.js +0 -83
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.
|
|
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.
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
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
|
|
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
|
|
101
|
-
unmet is `partial`. A truthful `partial` is a
|
|
102
|
-
what an agent reports instead of stretching
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
git commit -m "feat: account recovery" \
|
|
110
|
+
-m "Inkan-Outcome: 2026-09-03-0621-82qz"
|
|
119
111
|
```
|
|
120
112
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
118
|
+
The outcome is closed. There is no delivery audit to run afterwards.
|
|
130
119
|
|
|
131
|
-
|
|
132
|
-
the same trailer. The recorded tree no longer matches the commit:
|
|
120
|
+
## Commit references when reading history
|
|
133
121
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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.
|
|
191
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
loss and leave other sessions' outcomes alone; closed outcomes are
|
|
216
|
-
|
|
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
|
-
##
|
|
244
|
+
## One record through the work
|
|
253
245
|
|
|
254
|
-
|
|
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
|
|
265
|
-
|
|
|
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.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
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
|
|
286
|
-
| `inkan
|
|
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`
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
326
|
-
|
|
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
|
-
|
|
7
|
+
*先把工作要交付的结果 seal 下来,在结束时如实记录完成情况。*
|
|
8
8
|
|
|
9
|
-
Inkan 是一款小巧、零依赖的 CLI
|
|
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
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
**5. 把工作和记录一起提交。**
|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
把本项工作涉及的文件加入暂存区,包括 `.inkan/` 中的记录。落地 commit 要带上 `end` 打印的 trailer:
|
|
91
83
|
|
|
92
84
|
```sh
|
|
93
|
-
|
|
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
|
-
|
|
91
|
+
outcome 已经关闭,后面没有需要执行的交付审计。
|
|
105
92
|
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
- **它绝不充当 gate。** `git commit` 之前或期间不会运行任何 Inkan 操作,`init` 也不会安装 hook
|
|
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
|
|
142
|
+
`inkan init` 会把生成好的 protocol block 写进 coding agent 本来就会读取的 `AGENTS.md`。其中只有五条规则:在 durable change 之前 seal;seal 是事实;先逐项 disposition 并关闭,再把记录与工作一起提交,并写入 outcome trailer;context 丢失后用 `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
|
-
|
|
166
|
+
每个阶段都向记录追加内容,不改写此前的声明。
|
|
185
167
|
|
|
186
168
|
| 阶段 | 记录什么 | 命令 |
|
|
187
169
|
|---|---|---|
|
|
188
170
|
| 意图 | 要交付什么,以及如何判断是否达成 | `begin` |
|
|
189
171
|
| 变更 | 意图如何变化,以及为什么变化 | `amend --reason` |
|
|
190
172
|
| 约束 | 当前工作受哪些 decision 约束 | `decision add`、`--decision` |
|
|
191
|
-
| 关闭 | 每条标准的 disposition
|
|
192
|
-
|
|
|
193
|
-
| 审计 | commit 与记录是否仍然一致 | `check`、`doctor` |
|
|
173
|
+
| 关闭 | 每条标准的 disposition,以及由这些声明推导出的状态 | `end` |
|
|
174
|
+
| 提交关联 | commit 关联的 outcome,供阅读时了解上下文 | `Inkan-Outcome` trailer |
|
|
194
175
|
| 恢复 | 新 session 从哪里接手 | `status`、`log` |
|
|
195
176
|
|
|
196
|
-
Inkan
|
|
177
|
+
Inkan 本身也这样开发:工作先作为 outcome 被 seal,结束时记录声明;设计决定保存在 `.inkan/decisions/` 中。仓库保存长期上下文,供参与工作的每个 agent 使用。
|
|
178
|
+
|
|
179
|
+
## 可选的文件诊断
|
|
180
|
+
|
|
181
|
+
`inkan doctor` 报告记录文件损坏、id 不匹配或重复,以及缺失的 decision 引用。需要排查这些文件时可以主动运行;它不判断工作结果、不读取 commit,也不修复内容。
|
|
197
182
|
|
|
198
|
-
|
|
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
|
|
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
|
|
210
|
-
| `inkan
|
|
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`
|
|
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
|
|
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
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,
|
|
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
|
-
// ---
|
|
290
|
+
// --- doctor ----------------------------------------------------------------
|
|
300
291
|
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
decision ids, and dangling decision links.
|
|
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
|
-
}
|