diarium 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FLOW.md +209 -0
- package/INTEGRATE.md +13 -0
- package/LICENSE +202 -0
- package/README.md +63 -0
- package/bin/diarium.mjs +330 -0
- package/package.json +41 -0
package/FLOW.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# The flow
|
|
2
|
+
|
|
3
|
+
Every command output below is real, copied from a live run against a repo with a bd tracker and a GitHub tracker — not
|
|
4
|
+
an illustration.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
npm i -D diarium
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
First run creates a `diarium/` folder next to your code and generates a signing key into `.env`:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
diarium/
|
|
16
|
+
rules.md prose. The agent reads it before every entry. Yours to rewrite — we do not care how you word it.
|
|
17
|
+
settings.json structure. Code reads it: which trackers to watch, where the cursors are.
|
|
18
|
+
.pending/ closures that owe an entry
|
|
19
|
+
*.ust.json one sealed entry per file
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The key lives in `.env` as `DIARIUM_SEED` and is never printed — not even by the tool that made it. Lose it and the
|
|
23
|
+
chain can no longer be extended, though every existing entry still verifies. Leak it and someone else can write entries
|
|
24
|
+
as you, and every one of those will verify. So the tool checks, on every run, whether git actually ignores `.env`:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
· generated a signing key into .env — gitignore it. The seed is never printed, not even here.
|
|
28
|
+
! .env is NOT gitignored — add it now. The seed in there is the identity of this store;
|
|
29
|
+
anyone who has it can write entries as you, and every one of them will verify.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The key is generated at install rather than at the first entry, so that warning arrives while you are setting up — not
|
|
33
|
+
half an hour later, mid-flow, when the store already exists.
|
|
34
|
+
|
|
35
|
+
## Wire it to your agent
|
|
36
|
+
|
|
37
|
+
There is no plugin, no MCP server, no GitHub Action. Your agent already runs shell commands, so one paragraph in
|
|
38
|
+
whatever file it reads as standing instructions — `CLAUDE.md`, `AGENTS.md`, a Cursor rule — is the whole integration.
|
|
39
|
+
The suggested wording is in `INTEGRATE.md`.
|
|
40
|
+
|
|
41
|
+
## The loop
|
|
42
|
+
|
|
43
|
+
### 1. Start of a session — what did I learn last time
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
$ diarium read --depth 3
|
|
47
|
+
walking back 3 hop(s) from the head of 9 entries
|
|
48
|
+
|
|
49
|
+
── ust:20260725.154843 · task diarium-ixr · 20260725.154843_diarium-ixr_be8ef642.ust.json
|
|
50
|
+
Nothing here to learn: this task existed only to prove the trigger fires on a real closure. Recording that
|
|
51
|
+
plainly rather than inventing an insight.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It walks the chain N hops back from the head rather than loading the whole corpus, so a store with a thousand entries
|
|
55
|
+
costs the same to read as one with three.
|
|
56
|
+
|
|
57
|
+
### 2. Work. Close a task — in whichever tracker you use
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
$ bd close UST-l0a
|
|
61
|
+
```
|
|
62
|
+
or
|
|
63
|
+
```
|
|
64
|
+
$ gh issue close 90
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Nothing special: close it the way you always do.
|
|
68
|
+
|
|
69
|
+
### 3. `scan` — what closed since last time
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ diarium scan
|
|
73
|
+
bd:..: 1 closure(s) since 2026-07-25T15:56
|
|
74
|
+
github:thelabmd/UST-Protocol: 0 closure(s) since 2026-07-25T15:56
|
|
75
|
+
|
|
76
|
+
1 new obligation(s) — run status
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Both trackers answer the same question — *what closed after this cursor* — so adding a tracker is an adapter, not an
|
|
80
|
+
integration. A tracker that is unreachable says so and its cursor is left untouched, so nothing is skipped quietly.
|
|
81
|
+
|
|
82
|
+
**The very first scan sets a baseline instead of a backlog:**
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
bd:..: BASELINE set — 79 historical closure(s) skipped (a memory you did not live is an invention, not a memory)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Asking an agent to write seventy-nine entries about work it does not remember would manufacture recollection. What is
|
|
89
|
+
skipped is printed, not dropped in silence.
|
|
90
|
+
|
|
91
|
+
### 4. `status` — what owes an entry
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
$ diarium status
|
|
95
|
+
store: 9 entries · cap 560 · diarium/
|
|
96
|
+
✗ 1 closure(s) owe an entry — a closure without an entry is what this catches:
|
|
97
|
+
UST-l0a done 2026-07-25T16:09 flow doc demo — a real closure in this repo tracker
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Exit code is 1 while anything is owed, so it can gate a commit if you want it to.
|
|
101
|
+
|
|
102
|
+
This is the load-bearing part of the design: **the agent does not choose when to write.** A closure creates the
|
|
103
|
+
obligation; only the agent can discharge it. It cannot skip a closure it would rather not record, and it cannot bury
|
|
104
|
+
one entry under ten others.
|
|
105
|
+
|
|
106
|
+
### 5. The agent writes the recap
|
|
107
|
+
|
|
108
|
+
What it should write: **what it understood and what it learned** — not what it did. Git already keeps what it did, and
|
|
109
|
+
keeps it better.
|
|
110
|
+
|
|
111
|
+
If it no longer holds the task, the honest entry is exactly that, and it is a first-class record:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
$ diarium write UST-l0a recap.md --nothing-learned
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Absence of a result is a result. It is recorded structurally rather than left inside a sentence, so a corpus that is
|
|
118
|
+
mostly *nothing learned* can be counted — and that count tells you something about the work, or about the trigger.
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
$ diarium write UST-l0a recap.md
|
|
122
|
+
✓ entry sealed + stored
|
|
123
|
+
file : 20260725.160942_UST-l0a_6e96103b.ust.json
|
|
124
|
+
task : UST-l0a (tracker-local)
|
|
125
|
+
content_hash: sha256:6e96103b582a92dc7312ccea9d86d03adc186c818f76bde8da51569beddaf975
|
|
126
|
+
prev : sha256:be8ef64235d1919de4bd88f2d78c32b27c2373328fbd83c8fa8e44d705d4dcae
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The entry is verified **before** it is stored, chained to the head of the chain (not to the last filename), and the
|
|
130
|
+
obligation is discharged. The cap declared in `rules.md` is enforced here; everything else in that file the tool can
|
|
131
|
+
only ask for, which is honest as long as asking is not dressed up as enforcement.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
$ diarium status
|
|
135
|
+
store: 10 entries · cap 560 · diarium/
|
|
136
|
+
✓ no closure is waiting for an entry
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 6. `verify` — nobody rewrote anything
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
$ diarium verify
|
|
143
|
+
✓ 10 entries: every seal verifies, one genesis, one head, no fork, no orphan
|
|
144
|
+
(order from the chain — filenames are cosmetic)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This is the only reason the entries are UST documents rather than lines in a text file. It catches, with the sealed
|
|
148
|
+
bytes alone:
|
|
149
|
+
|
|
150
|
+
| what happened | what verify says |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| an entry's text was edited | that seal does not verify |
|
|
153
|
+
| an entry was deleted from the middle | no genesis, and a `prev` that resolves to nothing |
|
|
154
|
+
| entries were reordered by renaming | nothing — names carry no meaning, and that is by design |
|
|
155
|
+
| an entry was duplicated | a fork: two entries claim the same `prev` |
|
|
156
|
+
| the whole folder was moved | nothing — a seal is not bound to where it is stored |
|
|
157
|
+
|
|
158
|
+
An entry cannot be rewritten afterwards **including by the agent that wrote it**, and anyone can check that offline
|
|
159
|
+
without trusting you. Hand someone a single file and they can verify it with the public web verifier or `npm i
|
|
160
|
+
ust-protocol` — no server, no account, no call back to us.
|
|
161
|
+
|
|
162
|
+
## What a stored entry actually is
|
|
163
|
+
|
|
164
|
+
A plain UST document. Not a diarium format:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"ust": "1.0",
|
|
169
|
+
"state": {
|
|
170
|
+
"id": { "domain_shard": "sha256:9075…", "ust_id": "ust:20260725.160942",
|
|
171
|
+
"key_id": "sha256:9075…", "class": "observation" },
|
|
172
|
+
"time": { "generated_at": "2026-07-25T16:09:42Z", "valid_from": "…", "valid_to": "…" },
|
|
173
|
+
"data": { "entry": { "kind": "captured", "value": {
|
|
174
|
+
"text": "…the recap…",
|
|
175
|
+
"task": { "ref": "thelabmd/UST-Protocol#91", "id": "4975299807",
|
|
176
|
+
"node_id": "I_kwDO…", "source": "github",
|
|
177
|
+
"closed_at": "2026-07-25T16:09:12Z" } } } },
|
|
178
|
+
"hashes": { "entry": "sha256:aaa7…" },
|
|
179
|
+
"provenance": { "prev": "sha256:be8e…" }
|
|
180
|
+
},
|
|
181
|
+
"sig": { "alg": "Ed25519", "key_id": "sha256:9075…", "pub": "56tM…", "sig": "pIsn…" }
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Two details worth knowing:
|
|
186
|
+
|
|
187
|
+
**The task reference carries a durable id.** `owner/repo#N` is a path, and paths go stale — rename or transfer the repo
|
|
188
|
+
and the reference inside an already-sealed entry becomes unresolvable, with no way to fix it. So the seal carries the
|
|
189
|
+
readable ref *and* the identifier that survives a rename. Where no global id exists — bd, any local tracker — that is
|
|
190
|
+
recorded as `source: tracker-local` rather than implied, and nothing is invented when a lookup fails.
|
|
191
|
+
|
|
192
|
+
**Lived and reconstructed are told apart by evidence, not by claim.** The seal carries the closure time next to the
|
|
193
|
+
write time, so the interval is observable: an entry written three minutes after the closure was lived, one written
|
|
194
|
+
three days later was reconstructed. Nobody declares which — declaring your own reliability is exactly the self-asserted
|
|
195
|
+
assurance this design refuses everywhere else.
|
|
196
|
+
|
|
197
|
+
## Filenames
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
20260725.160942_UST-l0a_6e96103b.ust.json
|
|
201
|
+
└── address ──┘ └ task ┘ └ hash ┘
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Underscore separates fields and hyphen lives inside them, because repo names carry hyphens (`UST-Protocol`) and a
|
|
205
|
+
separator that can also appear in the data is not a separator. The address has no `ust:` prefix because a colon cannot
|
|
206
|
+
be checked out on Windows.
|
|
207
|
+
|
|
208
|
+
None of this carries meaning. Order comes from the chain, identity from the `content_hash` — rename every file and
|
|
209
|
+
`verify` stays green. The name is there so a human can find something by eye.
|
package/INTEGRATE.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Wire it to your agent
|
|
2
|
+
|
|
3
|
+
There is no plugin, no MCP server, no GitHub Action. Your agent already runs shell commands, so one instruction is
|
|
4
|
+
enough. Paste this into whatever file your agent reads as standing instructions — `CLAUDE.md`, `AGENTS.md`, a Cursor
|
|
5
|
+
rule, a system prompt. We suggest the wording; how you wire it is yours.
|
|
6
|
+
|
|
7
|
+
> **Diary.** At the start of a session run `diarium scan` — it lists tasks that closed since you last looked. Each one
|
|
8
|
+
> owes an entry. Write what you UNDERSTOOD and what you LEARNED, not what you did, and seal it with
|
|
9
|
+
> `diarium write <task-ref> <file>`. If you no longer hold the task, say exactly that and pass `--nothing-learned` —
|
|
10
|
+
> absence of a result is a result. Do not read the issue and the diff to reconstruct an insight; git already keeps
|
|
11
|
+
> summaries better than you will. Before starting work, `diarium read --depth 3` to see what you last learned.
|
|
12
|
+
|
|
13
|
+
That is the whole integration.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# diarium
|
|
2
|
+
|
|
3
|
+
Agent memory as a verifiable UST stream.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm i -D diarium
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
A task closes in your tracker. The agent writes what it understood and what it learned from it — a few sentences, its own voice. That text is sealed as a UST `observation`, `prev`-chained to the entry before it, and appended to `diarium/`. The agent reads it back later as its own memory, walking the chain instead of loading the whole corpus.
|
|
10
|
+
|
|
11
|
+
Nothing here proves the work was done well. A sealed entry proves **the agent said this, then**, and that nobody — including the agent — rewrote it afterwards. Fixation, not truth. Ordering and non-repudiation are the product; correctness is not on offer.
|
|
12
|
+
|
|
13
|
+
The full walkthrough, with real command output, is in [FLOW.md](./FLOW.md). Wiring it to your agent is one paragraph: [INTEGRATE.md](./INTEGRATE.md).
|
|
14
|
+
|
|
15
|
+
## Why it is not just a log file
|
|
16
|
+
|
|
17
|
+
A log can be edited. A tracker comment can be edited. A closed issue can be reopened and its history rewritten. A `prev`-chained stream of signed entries cannot: change one entry and every later `prev` stops resolving, so the tampering is visible to anyone holding a later entry — no server, no trust in the author.
|
|
18
|
+
|
|
19
|
+
Hand someone a single file and they can check it offline with `npm i ust-protocol` or the public web verifier. Nothing calls back to us; there is no us to call.
|
|
20
|
+
|
|
21
|
+
## What holds it together
|
|
22
|
+
|
|
23
|
+
Nobody reviews an entry before it lands. That is deliberate, so the discipline cannot be review — it is the **trigger**: the agent does not choose when to write. A task closes, an entry is owed. It cannot skip a closure it would rather not record, and it cannot bury one entry under ten others. One closure, one entry, and a closure without an entry is detectable — that is what `status` is, and why it exits non-zero while anything is owed.
|
|
24
|
+
|
|
25
|
+
The rest is mechanical, and split honestly between what code can enforce and what only prose can ask for:
|
|
26
|
+
|
|
27
|
+
| enforced by code | asked for by the rules |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| character cap | write the failures too, not just the wins |
|
|
30
|
+
| `prev` chain, signature | one entry, one moment |
|
|
31
|
+
| one entry per closure | no performed feelings |
|
|
32
|
+
| a broken store cannot be extended | do not reconstruct a task you no longer hold |
|
|
33
|
+
|
|
34
|
+
The rules live in `diarium/rules.md` as prose, and the agent reads them before writing. Rewrite that file and the agent's behaviour changes — configuration by prose. The cap is yours to set. The append-only chain is not: without it this is a text file.
|
|
35
|
+
|
|
36
|
+
`"Nothing to learn"` is a first-class entry, recorded structurally rather than buried in a sentence, so a corpus that is mostly *nothing learned* can be counted. Absence of a result is a result.
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
diarium scan what closed since last time (first run sets a baseline, not a backlog)
|
|
42
|
+
diarium status closures owing an entry — exit 1 while any is owed, so it can gate
|
|
43
|
+
diarium write <ref> <file> seal + chain + store [--nothing-learned]
|
|
44
|
+
diarium read [--depth N] walk the chain N hops back from the head
|
|
45
|
+
diarium verify every seal, one genesis, one head, no fork, no orphan
|
|
46
|
+
diarium render markdown to stdout, derived, never stored
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Trackers are detected on first run — a `.beads/` directory, a GitHub remote — and written into `diarium/settings.json` for you to edit. Both answer one question, *what closed after this cursor*, so another tracker is an adapter, not an integration.
|
|
50
|
+
|
|
51
|
+
## Keys
|
|
52
|
+
|
|
53
|
+
A signing key is generated into `.env` as `DIARIUM_SEED` on first run and is never printed, not even by the tool that made it. Every run checks that git actually ignores it. Lose it and the chain can no longer be extended, though every existing entry still verifies. Leak it and someone else can write entries as you — and every one of those will verify too, which is the point of checking.
|
|
54
|
+
|
|
55
|
+
## Tier
|
|
56
|
+
|
|
57
|
+
`VALID:LIGHT`, key-form identity, and that is the right ceiling. Everyone keeps their own memories, so there is no third party to convince — no domain-bound identity to prove *who*, no external clock to prove *when*. Adding those would be selling assurance nobody asked for.
|
|
58
|
+
|
|
59
|
+
## Status
|
|
60
|
+
|
|
61
|
+
`0.1.0`, and the version is deliberate: everything else in this repository is protocol surface on the `1.0.0-rc` line, while this is a product built on top of it. Treat the CLI surface as unsettled and the sealed entries as permanent — an entry written today verifies under any later version, because what makes it verifiable is the protocol, not this tool.
|
|
62
|
+
|
|
63
|
+
Built on [`ust-protocol`](../ust-protocol), which is its only dependency, and which has none of its own.
|
package/bin/diarium.mjs
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// diarium — a task closes, the agent writes what it learned, the entry is sealed as a UST and chained into diarium/.
|
|
4
|
+
//
|
|
5
|
+
// The crypto is not here: it is `ust-protocol`, and this tool calls five functions from it. What lives here is the
|
|
6
|
+
// DISCIPLINE the library cannot hold — prev taken from the head of the chain rather than the last filename, the entry
|
|
7
|
+
// verified before it is stored, the cap enforced, one closure one entry, and a refusal to extend a broken store. Those
|
|
8
|
+
// are exactly the invariants an agent re-deriving them each session gets wrong.
|
|
9
|
+
//
|
|
10
|
+
// Everything is relative to the directory you run it in, so the store lands next to your code and the tool itself can
|
|
11
|
+
// live in node_modules.
|
|
12
|
+
import * as P from 'ust-protocol';
|
|
13
|
+
import { createPrivateKey, createPublicKey, randomBytes } from 'node:crypto';
|
|
14
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const CWD = process.cwd();
|
|
19
|
+
const STORE = join(CWD, 'diarium');
|
|
20
|
+
const PENDING = join(STORE, '.pending');
|
|
21
|
+
const RULES = join(STORE, 'rules.md');
|
|
22
|
+
const SETTINGS = join(STORE, 'settings.json');
|
|
23
|
+
const ENV = join(CWD, '.env');
|
|
24
|
+
|
|
25
|
+
// rules.md is PROSE and it is the agent's prompt — yours to rewrite, and the tool does not care how you word it.
|
|
26
|
+
// Only `cap` is parsed and enforced; the rest it can ask for, which is honest as long as asking is not presented as
|
|
27
|
+
// enforcement. settings.json is STRUCTURE, read by code: a typo there fails loudly instead of being ignored.
|
|
28
|
+
const DEFAULT_RULES = `# Rules
|
|
29
|
+
|
|
30
|
+
Yours to rewrite. The tool reads this file before every entry and enforces \`cap\`; the rest it can only ask for.
|
|
31
|
+
|
|
32
|
+
cap: 560
|
|
33
|
+
|
|
34
|
+
- Write what you UNDERSTOOD and what you LEARNED from the task that just closed. Not what you did — git already keeps that.
|
|
35
|
+
- Write the failures too. A task that went wrong, a fix that did not hold, a call you got wrong. Never dress a loss up as a win.
|
|
36
|
+
- One entry, one thing. If it does not fit the cap, cut it; do not split it.
|
|
37
|
+
- No performed feelings. If it reads like marketing, delete it and write what actually happened.
|
|
38
|
+
- Do NOT reconstruct. If you no longer hold the task, say so and stop — reading the issue and the diff to manufacture an
|
|
39
|
+
insight produces a summary, and git already keeps summaries better than you will. Read enough to RECOGNISE whether you
|
|
40
|
+
remember it; never enough to invent it.
|
|
41
|
+
- "Nothing to learn" is a real entry. Absence of a result is a result. Write it plainly and pass --nothing-learned.
|
|
42
|
+
- Nobody reviews this before it lands. The discipline is the trigger, not review: you do not choose when to write.
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
const cmd = args[0];
|
|
47
|
+
const flag = (n, d) => { const i = args.indexOf('--' + n); return i > 0 && args[i + 1] ? args[i + 1] : d; };
|
|
48
|
+
const die = (m) => { console.error('✗ ' + m); process.exit(1); };
|
|
49
|
+
|
|
50
|
+
// First run detects the trackers this repo actually has rather than asking you to describe them.
|
|
51
|
+
function detectSources() {
|
|
52
|
+
const out = [];
|
|
53
|
+
if (existsSync(join(CWD, '.beads'))) out.push({ type: 'bd', cwd: '.' });
|
|
54
|
+
try {
|
|
55
|
+
const url = execSync('git remote get-url origin', { cwd: CWD, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
56
|
+
const m = /[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/.exec(url);
|
|
57
|
+
if (m) out.push({ type: 'github', repo: `${m[1]}/${m[2]}` });
|
|
58
|
+
} catch { /* no remote: leave it out rather than guess */ }
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The seed is generated at INIT, not lazily on the first entry: the developer must see the "gitignore this" warning
|
|
63
|
+
// while installing, not in the middle of writing. A leaked seed lets someone else write entries as you.
|
|
64
|
+
function ignored() {
|
|
65
|
+
try { execSync('git check-ignore -q .env', { cwd: CWD, stdio: 'ignore' }); return true; } catch { return false; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ensure() {
|
|
69
|
+
mkdirSync(PENDING, { recursive: true });
|
|
70
|
+
if (!existsSync(RULES)) writeFileSync(RULES, DEFAULT_RULES);
|
|
71
|
+
if (!existsSync(SETTINGS)) {
|
|
72
|
+
const sources = detectSources();
|
|
73
|
+
writeFileSync(SETTINGS, JSON.stringify({ sources, cursors: {} }, null, 2) + '\n');
|
|
74
|
+
console.log(` · created ${STORE.replace(CWD + '/', '')}/ with ${sources.length} detected tracker(s): ${sources.map((s) => s.type).join(', ') || 'none — add them to settings.json'}`);
|
|
75
|
+
}
|
|
76
|
+
key();
|
|
77
|
+
if (!ignored()) console.log(' ! .env is NOT gitignored — add it now. The seed in there is the identity of this store;\n anyone who has it can write entries as you, and every one of them will verify.');
|
|
78
|
+
}
|
|
79
|
+
const cap = () => { const m = (existsSync(RULES) ? readFileSync(RULES, 'utf8') : DEFAULT_RULES).match(/^cap:\s*(\d+)/m); return m ? Number(m[1]) : 560; };
|
|
80
|
+
const files = () => (existsSync(STORE) ? readdirSync(STORE).filter((f) => f.endsWith('.ust.json')).sort() : []);
|
|
81
|
+
const load = (f) => JSON.parse(readFileSync(join(STORE, f), 'utf8'));
|
|
82
|
+
|
|
83
|
+
// Order comes from the CHAIN, never from the filesystem: filenames are cosmetic and renaming every file must change
|
|
84
|
+
// nothing. Deriving order from `prev` also sees forks and orphans, which a filename sort never can.
|
|
85
|
+
function chain() {
|
|
86
|
+
const docs = files().map((f) => ({ f, d: load(f) }));
|
|
87
|
+
const problems = [];
|
|
88
|
+
if (!docs.length) return { ordered: [], problems };
|
|
89
|
+
const hashOf = (x) => P.contentHash(x.d);
|
|
90
|
+
const prevOf = (x) => x.d.state.provenance?.prev;
|
|
91
|
+
const byHash = new Map(docs.map((x) => [hashOf(x), x]));
|
|
92
|
+
|
|
93
|
+
const genesis = docs.filter((x) => !prevOf(x));
|
|
94
|
+
if (!genesis.length) problems.push('no genesis: every entry carries a prev, so the oldest one is missing');
|
|
95
|
+
if (genesis.length > 1) problems.push(`${genesis.length} entries carry no prev — a stream has exactly one genesis: ${genesis.map((x) => x.f).join(', ')}`);
|
|
96
|
+
|
|
97
|
+
const claimed = new Map();
|
|
98
|
+
for (const x of docs) {
|
|
99
|
+
const p = prevOf(x); if (!p) continue;
|
|
100
|
+
if (!byHash.has(p)) problems.push(`${x.f}: prev ${p.slice(0, 20)}… resolves to no entry here — a gap, or the chain leaves this store`);
|
|
101
|
+
claimed.set(p, (claimed.get(p) || []).concat(x.f));
|
|
102
|
+
}
|
|
103
|
+
for (const [p, fs2] of claimed) if (fs2.length > 1) problems.push(`FORK: ${fs2.length} entries claim the same prev ${p.slice(0, 20)}… — ${fs2.join(', ')}`);
|
|
104
|
+
|
|
105
|
+
const heads = docs.filter((x) => !claimed.has(hashOf(x)));
|
|
106
|
+
if (heads.length > 1) problems.push(`${heads.length} heads — nothing follows any of: ${heads.map((x) => x.f).join(', ')}`);
|
|
107
|
+
|
|
108
|
+
const ordered = []; const seen = new Set();
|
|
109
|
+
let cur = heads[0], guard = docs.length + 1;
|
|
110
|
+
while (cur && guard-- > 0) {
|
|
111
|
+
if (seen.has(cur.f)) { problems.push(`cycle detected at ${cur.f}`); break; }
|
|
112
|
+
seen.add(cur.f); ordered.unshift(cur);
|
|
113
|
+
const p = prevOf(cur); cur = p ? byHash.get(p) : null;
|
|
114
|
+
}
|
|
115
|
+
for (const x of docs) if (!seen.has(x.f)) problems.push(`${x.f}: unreachable from the head — an orphan the chain does not include`);
|
|
116
|
+
return { ordered, problems };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function key() {
|
|
120
|
+
const env = existsSync(ENV) ? readFileSync(ENV, 'utf8') : '';
|
|
121
|
+
let m = env.match(/^DIARIUM_SEED=(\S+)/m);
|
|
122
|
+
if (!m) {
|
|
123
|
+
const seed = randomBytes(32).toString('base64url');
|
|
124
|
+
appendFileSync(ENV, `${env.endsWith('\n') || !env ? '' : '\n'}DIARIUM_SEED=${seed}\n`);
|
|
125
|
+
m = [null, seed];
|
|
126
|
+
console.log(' · generated a signing key into .env — gitignore it. The seed is never printed, not even here.');
|
|
127
|
+
}
|
|
128
|
+
const seed = Buffer.from(m[1], 'base64url');
|
|
129
|
+
const priv = createPrivateKey({ key: Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), seed]), format: 'der', type: 'pkcs8' });
|
|
130
|
+
const pub = createPublicKey(priv).export({ format: 'der', type: 'spki' }).slice(-32).toString('base64url');
|
|
131
|
+
return { priv, pub, kid: P.keyId(pub) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A task reference is a PATH and paths go stale: rename or transfer the repo and the reference inside an already-sealed
|
|
135
|
+
// entry is unresolvable, unfixable because the seal is immutable. So the seal carries the readable ref AND the id that
|
|
136
|
+
// survives a rename. Where no global id exists that is RECORDED, never implied, and nothing is invented on failure.
|
|
137
|
+
function resolveTask(ref) {
|
|
138
|
+
const m = /^(?:([\w.-]+)\/([\w.-]+))?#?(\d+)$/.exec(String(ref).trim());
|
|
139
|
+
if (!m) return { ref, source: /^[A-Za-z][\w.-]*-[a-z0-9]+$/.test(ref) ? 'tracker-local' : 'raw' };
|
|
140
|
+
let [, owner, repo, num] = m;
|
|
141
|
+
if (!owner || !repo) {
|
|
142
|
+
try {
|
|
143
|
+
const url = execSync('git remote get-url origin', { cwd: CWD, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
144
|
+
const rm = /[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/.exec(url);
|
|
145
|
+
if (rm) { owner = rm[1]; repo = rm[2]; }
|
|
146
|
+
} catch { /* stay unqualified rather than guess */ }
|
|
147
|
+
}
|
|
148
|
+
if (!owner || !repo) return { ref, source: 'raw' };
|
|
149
|
+
const qualified = `${owner}/${repo}#${num}`;
|
|
150
|
+
try {
|
|
151
|
+
const j = JSON.parse(execSync(`gh api repos/${owner}/${repo}/issues/${num} --jq '{id:.id,node_id:.node_id}'`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
|
|
152
|
+
return { ref: qualified, id: String(j.id), node_id: j.node_id, source: 'github' };
|
|
153
|
+
} catch { return { ref: qualified, source: 'github-unresolved' }; }
|
|
154
|
+
}
|
|
155
|
+
const field = (s) => String(s).replace(/_/g, '-').replace(/[^\w.-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
156
|
+
const slug = (t) => { const m = /^(?:[\w.-]+\/)?([\w.-]+)#(\d+)$/.exec(t.ref); return m ? `${field(m[1])}_${field(m[2])}` : field(t.ref); };
|
|
157
|
+
const pendingPath = (ref) => join(PENDING, String(ref).replace(/[^\w.-]/g, '_') + '.json');
|
|
158
|
+
|
|
159
|
+
// ── scan ─────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
// Every source answers ONE question — what closed after this cursor — so a new tracker is an adapter, not an
|
|
161
|
+
// integration. Polling is what makes the trigger real: measured on a live day, it caught every closure while a
|
|
162
|
+
// commit-keyword hook caught none, because the closures went through `gh issue close` and the commits said "Refs #90".
|
|
163
|
+
if (cmd === 'scan') {
|
|
164
|
+
ensure();
|
|
165
|
+
let cfg; try { cfg = JSON.parse(readFileSync(SETTINGS, 'utf8')); } catch (e) { die(`settings.json does not parse — a typo there fails loudly on purpose: ${e.message}`); }
|
|
166
|
+
cfg.cursors = cfg.cursors || {};
|
|
167
|
+
let found = 0;
|
|
168
|
+
for (const s of (cfg.sources || []).filter((x) => x.enabled !== false)) {
|
|
169
|
+
const id = `${s.type}:${s.repo || s.cwd || '.'}`;
|
|
170
|
+
const cold = !cfg.cursors[id];
|
|
171
|
+
const since = cfg.cursors[id] || '1970-01-01T00:00:00Z';
|
|
172
|
+
let rows = [];
|
|
173
|
+
try {
|
|
174
|
+
if (s.type === 'github') {
|
|
175
|
+
rows = JSON.parse(execSync(`gh issue list -R ${s.repo} --state closed --limit 100 --json number,closedAt,title`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }))
|
|
176
|
+
.filter((x) => x.closedAt > since).map((x) => ({ ref: `${s.repo}#${x.number}`, at: x.closedAt, title: x.title }));
|
|
177
|
+
} else if (s.type === 'bd') {
|
|
178
|
+
rows = JSON.parse(execSync('bd list --status=closed --limit 400 --json', { cwd: join(CWD, s.cwd || '.'), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }))
|
|
179
|
+
.filter((x) => (x.closed_at || '') > since).map((x) => ({ ref: x.id, at: x.closed_at, title: x.title }));
|
|
180
|
+
} else { console.log(` ? ${id}: unknown source type — skipped, never guessed`); continue; }
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// The CAUSE lives in stderr; e.message only repeats the command back, which is the least informative line there is.
|
|
183
|
+
const why = (String(e.stderr || '').split('\n').filter(Boolean)[0] || String(e.message).split('\n')[1] || 'no output').trim();
|
|
184
|
+
console.log(` ! ${id}: unreachable — cursor untouched, so nothing is skipped quietly\n ${why.slice(0, 120)}`); continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// A cold start sets a BASELINE, not a backlog. Asking an agent to write dozens of entries about work it does not
|
|
188
|
+
// remember manufactures recollection, and a corpus of invented memories is worse than an empty one.
|
|
189
|
+
if (cold) {
|
|
190
|
+
cfg.cursors[id] = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
191
|
+
console.log(` ${id}: BASELINE set — ${rows.length} historical closure(s) skipped (a memory you did not live is an invention, not a memory)`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
for (const r of rows.sort((a, b) => a.at.localeCompare(b.at))) {
|
|
195
|
+
if (!existsSync(pendingPath(r.ref))) { writeFileSync(pendingPath(r.ref), JSON.stringify({ ref: r.ref, kind: 'done', title: r.title || '', at: r.at }, null, 2) + '\n'); found++; }
|
|
196
|
+
if (r.at > (cfg.cursors[id] || '')) cfg.cursors[id] = r.at;
|
|
197
|
+
}
|
|
198
|
+
console.log(` ${id}: ${rows.length} closure(s) since ${since.slice(0, 16)}`);
|
|
199
|
+
}
|
|
200
|
+
writeFileSync(SETTINGS, JSON.stringify(cfg, null, 2) + '\n');
|
|
201
|
+
console.log(found ? `\n ${found} new obligation(s) — run: diarium status` : '\n nothing new');
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (cmd === 'closed') {
|
|
206
|
+
const ref = args[1]; if (!ref) die('usage: diarium closed <ref> [--kind done|cancelled] [--title "..."]');
|
|
207
|
+
ensure();
|
|
208
|
+
if (existsSync(pendingPath(ref))) { console.log(`· ${ref} already owes an entry`); process.exit(0); }
|
|
209
|
+
writeFileSync(pendingPath(ref), JSON.stringify({ ref, kind: flag('kind', 'done'), title: flag('title', ''), at: new Date().toISOString() }, null, 2) + '\n');
|
|
210
|
+
console.log(`· ${ref} closed — an entry is owed: diarium write ${ref} <file>`);
|
|
211
|
+
process.exit(0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── status: the gate. A closure without an entry is the thing this catches, so it exits non-zero while any is owed.
|
|
215
|
+
if (cmd === 'status') {
|
|
216
|
+
ensure();
|
|
217
|
+
const owed = readdirSync(PENDING).filter((f) => f.endsWith('.json')).map((f) => JSON.parse(readFileSync(join(PENDING, f), 'utf8')));
|
|
218
|
+
console.log(` store: ${files().length} entries · cap ${cap()} · diarium/`);
|
|
219
|
+
if (!owed.length) { console.log(' ✓ no closure is waiting for an entry'); process.exit(0); }
|
|
220
|
+
console.log(` ✗ ${owed.length} closure(s) owe an entry:`);
|
|
221
|
+
for (const o of owed.sort((a, b) => a.at.localeCompare(b.at))) console.log(` ${String(o.ref).padEnd(16)} ${o.kind.padEnd(9)} ${o.at.slice(0, 16)} ${(o.title || '').slice(0, 60)}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (cmd === 'write') {
|
|
226
|
+
const ref = args[1], bodyPath = args[2];
|
|
227
|
+
if (!ref || !bodyPath || !existsSync(bodyPath)) die('usage: diarium write <task-ref> <file> [--nothing-learned]');
|
|
228
|
+
ensure();
|
|
229
|
+
const body = readFileSync(bodyPath, 'utf8').trim();
|
|
230
|
+
if (!body) die('empty entry');
|
|
231
|
+
const limit = cap();
|
|
232
|
+
if (body.length > limit) die(`entry is ${body.length} characters — the cap declared in diarium/rules.md is ${limit}. Cut it, do not split it.`);
|
|
233
|
+
// "Nothing to learn" is first-class and recorded STRUCTURALLY so it can be counted: a corpus that is mostly this is
|
|
234
|
+
// telling you something, and that signal is lost if the fact hides inside a sentence.
|
|
235
|
+
const nothing = args.includes('--nothing-learned');
|
|
236
|
+
|
|
237
|
+
const task = resolveTask(ref);
|
|
238
|
+
// The closure time travels into the seal so the closure→entry INTERVAL is observable: an entry written minutes after
|
|
239
|
+
// the closure was lived, one written days later was reconstructed. Nobody declares which — declaring your own
|
|
240
|
+
// reliability is the self-asserted assurance this design refuses everywhere else.
|
|
241
|
+
if (existsSync(pendingPath(ref))) { try { task.closed_at = JSON.parse(readFileSync(pendingPath(ref), 'utf8')).at; } catch { /* keep going without it */ } }
|
|
242
|
+
|
|
243
|
+
const { ordered, problems } = chain();
|
|
244
|
+
if (problems.length) { console.error('✗ refusing to extend a broken store — fix it first:'); for (const p of problems) console.error(' • ' + p); process.exit(1); }
|
|
245
|
+
const prev = ordered.length ? P.contentHash(ordered[ordered.length - 1].d) : undefined;
|
|
246
|
+
|
|
247
|
+
const { priv, pub, kid } = key();
|
|
248
|
+
const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
249
|
+
const ust_id = `ust:${now.slice(0, 4)}${now.slice(5, 7)}${now.slice(8, 10)}.${now.slice(11, 13)}${now.slice(14, 16)}${now.slice(17, 19)}`;
|
|
250
|
+
const state = P.buildState({ domain_shard: kid, ust_id, key_id: kid, class: 'observation' },
|
|
251
|
+
{ generated_at: now, valid_from: now, valid_to: now },
|
|
252
|
+
{ entry: { kind: 'captured', value: nothing ? { text: body, task, learned: 'none' } : { text: body, task } } },
|
|
253
|
+
prev ? { prev } : undefined);
|
|
254
|
+
const doc = P.seal(state, priv, pub);
|
|
255
|
+
const v = P.verify(doc, { context: 'data' });
|
|
256
|
+
if (v.result !== 'VALID:LIGHT') die(`entry did not seal VALID:LIGHT: ${v.result || v.error}`);
|
|
257
|
+
|
|
258
|
+
// Underscore separates fields, hyphen lives inside them: repo names carry hyphens, and a separator that can also
|
|
259
|
+
// appear in the data is not a separator. No `ust:` prefix — a colon cannot be checked out on Windows. The name
|
|
260
|
+
// carries no meaning; order is the chain's and identity is the content_hash.
|
|
261
|
+
const name = `${ust_id.slice(4)}_${slug(task)}_${P.contentHash(doc).slice(7, 15)}.ust.json`;
|
|
262
|
+
writeFileSync(join(STORE, name), JSON.stringify(doc) + '\n');
|
|
263
|
+
if (existsSync(pendingPath(ref))) unlinkSync(pendingPath(ref));
|
|
264
|
+
console.log('✓ entry sealed + stored');
|
|
265
|
+
console.log(' file :', name);
|
|
266
|
+
if (nothing) console.log(' learned : none (recorded as such, and countable)');
|
|
267
|
+
console.log(' task :', task.ref, `(${task.source}${task.id ? ', id ' + task.id : ''})`);
|
|
268
|
+
console.log(' content_hash:', P.contentHash(doc));
|
|
269
|
+
console.log(' prev :', prev || '(genesis)');
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (cmd === 'read') {
|
|
274
|
+
const depth = Number(flag('depth', '3'));
|
|
275
|
+
const { ordered, problems } = chain();
|
|
276
|
+
if (!ordered.length) { console.log(' (no entries yet)'); process.exit(0); }
|
|
277
|
+
if (problems.length) console.log(` ! ${problems.length} structural problem(s) — run: diarium verify\n`);
|
|
278
|
+
console.log(` walking back ${depth} hop(s) from the head of ${ordered.length} entries\n`);
|
|
279
|
+
for (const { f, d } of ordered.slice(-depth).reverse()) {
|
|
280
|
+
const st = d.state, v = st.data.entry?.value ?? {};
|
|
281
|
+
console.log(` ── ${st.id.ust_id} · task ${v.task?.ref ?? v.task ?? '?'}${v.learned === 'none' ? ' · learned: none' : ''}`);
|
|
282
|
+
console.log(' ' + String(v.text ?? '').split('\n').join('\n ') + '\n');
|
|
283
|
+
}
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (cmd === 'verify') {
|
|
288
|
+
const { ordered, problems } = chain();
|
|
289
|
+
const present = files();
|
|
290
|
+
if (!present.length) { console.log(' (no entries yet)'); process.exit(0); }
|
|
291
|
+
// Every file PRESENT, not just the chain-reachable ones: a tampered entry falls out of the chain, and verifying only
|
|
292
|
+
// what is reachable would leave its broken seal unreported.
|
|
293
|
+
const broken = [];
|
|
294
|
+
for (const f of present) {
|
|
295
|
+
let d; try { d = load(f); } catch { broken.push(`${f}: not parseable JSON`); continue; }
|
|
296
|
+
const v = P.verify(d, { context: 'data' });
|
|
297
|
+
if (v.result !== 'VALID:LIGHT') broken.push(`${f}: seal does NOT verify — ${v.result || v.error}`);
|
|
298
|
+
}
|
|
299
|
+
// Broken seals lead. A tampered entry is the FINDING; the orphans and extra heads it produces are consequences, and
|
|
300
|
+
// printing them first buries the one line that says what actually happened.
|
|
301
|
+
const fails = [...broken, ...problems];
|
|
302
|
+
if (fails.length) {
|
|
303
|
+
console.error(`✗ verify FAILED (${ordered.length} in chain, ${present.length} file(s) present):`);
|
|
304
|
+
for (const x of broken) console.error(' • ' + x);
|
|
305
|
+
if (broken.length && problems.length) console.error(' — and the structural consequences of that:');
|
|
306
|
+
for (const x of problems) console.error(' • ' + x);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
console.log(`✓ ${ordered.length} entries: every seal verifies, one genesis, one head, no fork, no orphan (order from the chain — filenames are cosmetic)`);
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (cmd === 'render') {
|
|
314
|
+
for (const { f, d } of chain().ordered) {
|
|
315
|
+
const st = d.state, v = st.data.entry?.value ?? {};
|
|
316
|
+
console.log(`${v.text}\n\n<sub>${f} · task ${v.task?.ref ?? v.task} · ${st.time.generated_at}</sub>\n\n---\n`);
|
|
317
|
+
}
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
console.log(`diarium — a task closes, you write what you learned, it is sealed and chained
|
|
322
|
+
|
|
323
|
+
diarium scan what closed since last time (first run sets a baseline)
|
|
324
|
+
diarium status closures owing an entry (exit 1 while any is owed)
|
|
325
|
+
diarium write <ref> <file> seal + chain + store [--nothing-learned]
|
|
326
|
+
diarium read [--depth N] walk the chain N hops back from the head
|
|
327
|
+
diarium verify every seal, one genesis, one head, no fork, no orphan
|
|
328
|
+
diarium render markdown to stdout, derived, never stored
|
|
329
|
+
|
|
330
|
+
The store is ./diarium/ — rules.md is prose you may rewrite, settings.json is structure.`);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "diarium",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent memory as a verifiable UST stream — a task closes, the agent writes what it learned, the entry is sealed and prev-chained. Tamper-evident, offline-verifiable, no server.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"diarium": "./bin/diarium.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"LICENSE",
|
|
12
|
+
"README.md",
|
|
13
|
+
"FLOW.md",
|
|
14
|
+
"INTEGRATE.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ust",
|
|
18
|
+
"agent",
|
|
19
|
+
"agent-memory",
|
|
20
|
+
"journal",
|
|
21
|
+
"tamper-evident",
|
|
22
|
+
"ed25519",
|
|
23
|
+
"provenance"
|
|
24
|
+
],
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"ust-protocol": "^1.0.0-rc.32"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/thelabmd/UST-Protocol.git",
|
|
35
|
+
"directory": "packages/diarium"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/thelabmd/UST-Protocol/tree/main/packages/diarium#readme",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node --test"
|
|
40
|
+
}
|
|
41
|
+
}
|