create-evda 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/bin/evda.mjs +382 -0
- package/create-evda.mjs +184 -0
- package/lib/agents.mjs +90 -0
- package/lib/circles.mjs +317 -0
- package/lib/doctor.mjs +419 -0
- package/lib/errors.mjs +26 -0
- package/lib/folders.mjs +160 -0
- package/lib/grants.mjs +188 -0
- package/lib/index.mjs +67 -0
- package/lib/ledger.mjs +168 -0
- package/lib/locate.mjs +76 -0
- package/lib/manifest.mjs +181 -0
- package/lib/migrate.mjs +66 -0
- package/lib/record.mjs +80 -0
- package/lib/references.mjs +283 -0
- package/lib/resolvers.mjs +123 -0
- package/lib/roles.mjs +89 -0
- package/lib/validate.mjs +84 -0
- package/lib/vectorcli.mjs +49 -0
- package/lib/vectors.mjs +53 -0
- package/lib/yaml.mjs +352 -0
- package/migrations/README.md +20 -0
- package/package.json +31 -0
- package/templates/AGENTS.md +170 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/circles-README.md +27 -0
- package/templates/evda-digest.md +35 -0
- package/templates/hypotheses.md +70 -0
package/lib/yaml.mjs
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// A YAML reader and writer for eco manifests, and for nothing else.
|
|
2
|
+
//
|
|
3
|
+
// The substrate writes an eco as `eco.yaml`, and this package has no runtime dependencies, so the
|
|
4
|
+
// subset of YAML an eco manifest uses is read and written here: comments, block mappings, block
|
|
5
|
+
// sequences, plain and quoted scalars, block scalars (`|`, `>`), and one level of flow collections.
|
|
6
|
+
// Anchors, aliases, tags, multi-document streams and tab indentation are refused with a line
|
|
7
|
+
// number rather than guessed at. `test/yaml.test.mjs` checks this reader against the `yaml`
|
|
8
|
+
// package over the substrate's own manifests, so the two agree on every file that matters.
|
|
9
|
+
|
|
10
|
+
import { EvdaError } from "./errors.mjs";
|
|
11
|
+
|
|
12
|
+
class Line {
|
|
13
|
+
constructor(indent, text, no) {
|
|
14
|
+
this.indent = indent;
|
|
15
|
+
this.text = text;
|
|
16
|
+
this.no = no;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fail(no, msg) {
|
|
21
|
+
throw new EvdaError(`yaml line ${no}: ${msg}`, { code: "BAD_YAML", exit: 1 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Strip a trailing comment from a line that is not inside quotes.
|
|
25
|
+
function stripComment(s) {
|
|
26
|
+
let inS = false;
|
|
27
|
+
let inD = false;
|
|
28
|
+
for (let i = 0; i < s.length; i++) {
|
|
29
|
+
const c = s[i];
|
|
30
|
+
if (c === "'" && !inD) inS = !inS;
|
|
31
|
+
else if (c === '"' && !inS && s[i - 1] !== "\\") inD = !inD;
|
|
32
|
+
else if (c === "#" && !inS && !inD && (i === 0 || /\s/.test(s[i - 1]))) return s.slice(0, i).trimEnd();
|
|
33
|
+
}
|
|
34
|
+
return s.trimEnd();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function lines(text) {
|
|
38
|
+
const out = [];
|
|
39
|
+
text.split("\n").forEach((raw, i) => {
|
|
40
|
+
const no = i + 1;
|
|
41
|
+
if (raw.startsWith("---") && i === 0) return;
|
|
42
|
+
if (raw.startsWith("\t")) fail(no, "tab indentation");
|
|
43
|
+
const indent = raw.length - raw.trimStart().length;
|
|
44
|
+
const body = stripComment(raw.slice(indent));
|
|
45
|
+
if (body === "") return;
|
|
46
|
+
out.push(new Line(indent, body, no));
|
|
47
|
+
});
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PLAIN_BOOL = { true: true, false: false, True: true, False: false, TRUE: true, FALSE: false };
|
|
52
|
+
const NULLS = new Set(["null", "Null", "NULL", "~", ""]);
|
|
53
|
+
|
|
54
|
+
export function scalar(s, no) {
|
|
55
|
+
const t = s.trim();
|
|
56
|
+
if (t.startsWith('"')) {
|
|
57
|
+
if (!t.endsWith('"') || t.length < 2) fail(no, "unterminated double-quoted string");
|
|
58
|
+
return t.slice(1, -1).replace(/\\(["\\/bfnrt]|u[0-9a-fA-F]{4})/g, (_, e) => {
|
|
59
|
+
if (e[0] === "u") return String.fromCharCode(parseInt(e.slice(1), 16));
|
|
60
|
+
return { '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" }[e];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (t.startsWith("'")) {
|
|
64
|
+
if (!t.endsWith("'") || t.length < 2) fail(no, "unterminated single-quoted string");
|
|
65
|
+
return t.slice(1, -1).replace(/''/g, "'");
|
|
66
|
+
}
|
|
67
|
+
if (t.startsWith("&") || t.startsWith("*") || t.startsWith("!")) fail(no, `anchors, aliases and tags are not supported: ${t}`);
|
|
68
|
+
if (t in PLAIN_BOOL) return PLAIN_BOOL[t];
|
|
69
|
+
if (NULLS.has(t)) return null;
|
|
70
|
+
if (/^[-+]?(0|[1-9]\d*)$/.test(t)) return Number(t);
|
|
71
|
+
if (/^[-+]?(\d+\.\d*|\.\d+|\d+)([eE][-+]?\d+)?$/.test(t)) return Number(t);
|
|
72
|
+
if (t.startsWith("[") || t.startsWith("{")) return flow(t, no);
|
|
73
|
+
if (/: |:$/.test(t)) fail(no, `a plain scalar may not contain ": "; quote it: ${t}`);
|
|
74
|
+
return t;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Flow collections: `[a, b]`, `{a: b, c: d}`, nested one level or more, scalars only inside.
|
|
78
|
+
function flow(t, no) {
|
|
79
|
+
let i = 0;
|
|
80
|
+
const peek = () => t[i];
|
|
81
|
+
const skip = () => { while (i < t.length && /\s/.test(t[i])) i++; };
|
|
82
|
+
const readScalar = (stops) => {
|
|
83
|
+
skip();
|
|
84
|
+
if (t[i] === '"' || t[i] === "'") {
|
|
85
|
+
const q = t[i];
|
|
86
|
+
let j = i + 1;
|
|
87
|
+
while (j < t.length) {
|
|
88
|
+
if (t[j] === q && (q === "'" ? t[j + 1] !== "'" : t[j - 1] !== "\\")) break;
|
|
89
|
+
if (q === "'" && t[j] === "'" && t[j + 1] === "'") j++;
|
|
90
|
+
j++;
|
|
91
|
+
}
|
|
92
|
+
if (j >= t.length) fail(no, "unterminated quoted string in flow collection");
|
|
93
|
+
const s = t.slice(i, j + 1);
|
|
94
|
+
i = j + 1;
|
|
95
|
+
return scalar(s, no);
|
|
96
|
+
}
|
|
97
|
+
let j = i;
|
|
98
|
+
while (j < t.length && !stops.includes(t[j])) j++;
|
|
99
|
+
const s = t.slice(i, j);
|
|
100
|
+
i = j;
|
|
101
|
+
return scalar(s, no);
|
|
102
|
+
};
|
|
103
|
+
const value = () => {
|
|
104
|
+
skip();
|
|
105
|
+
if (peek() === "[") {
|
|
106
|
+
i++;
|
|
107
|
+
const out = [];
|
|
108
|
+
skip();
|
|
109
|
+
if (peek() === "]") { i++; return out; }
|
|
110
|
+
for (;;) {
|
|
111
|
+
out.push(value());
|
|
112
|
+
skip();
|
|
113
|
+
if (peek() === ",") { i++; continue; }
|
|
114
|
+
if (peek() === "]") { i++; return out; }
|
|
115
|
+
fail(no, "expected , or ] in flow sequence");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (peek() === "{") {
|
|
119
|
+
i++;
|
|
120
|
+
const out = {};
|
|
121
|
+
skip();
|
|
122
|
+
if (peek() === "}") { i++; return out; }
|
|
123
|
+
for (;;) {
|
|
124
|
+
const k = readScalar([":"]);
|
|
125
|
+
skip();
|
|
126
|
+
if (peek() !== ":") fail(no, "expected : in flow mapping");
|
|
127
|
+
i++;
|
|
128
|
+
out[String(k)] = value();
|
|
129
|
+
skip();
|
|
130
|
+
if (peek() === ",") { i++; continue; }
|
|
131
|
+
if (peek() === "}") { i++; return out; }
|
|
132
|
+
fail(no, "expected , or } in flow mapping");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return readScalar([",", "]", "}"]);
|
|
136
|
+
};
|
|
137
|
+
const v = value();
|
|
138
|
+
skip();
|
|
139
|
+
if (i !== t.length) fail(no, `unexpected text after flow collection: ${t.slice(i)}`);
|
|
140
|
+
return v;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A key at the start of a block line. Returns {key, rest} or null when the line is not `key: ...`.
|
|
144
|
+
function splitKey(text, no) {
|
|
145
|
+
if (text.startsWith("- ") || text === "-") return null;
|
|
146
|
+
let i = 0;
|
|
147
|
+
let key;
|
|
148
|
+
if (text[0] === '"' || text[0] === "'") {
|
|
149
|
+
const q = text[0];
|
|
150
|
+
let j = 1;
|
|
151
|
+
while (j < text.length && text[j] !== q) j++;
|
|
152
|
+
if (j >= text.length) fail(no, "unterminated quoted key");
|
|
153
|
+
key = scalar(text.slice(0, j + 1), no);
|
|
154
|
+
i = j + 1;
|
|
155
|
+
} else {
|
|
156
|
+
const m = /^([^:#]+?)(?=:(?:\s|$))/.exec(text);
|
|
157
|
+
if (!m) return null;
|
|
158
|
+
key = m[1].trim();
|
|
159
|
+
i = m[1].length;
|
|
160
|
+
}
|
|
161
|
+
if (text[i] !== ":") return null;
|
|
162
|
+
return { key: String(key), rest: text.slice(i + 1).trim() };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function blockScalar(header, ls, i, parentIndent, no) {
|
|
166
|
+
const m = /^([|>])([-+]?)$/.exec(header);
|
|
167
|
+
if (!m) fail(no, `unsupported block scalar header ${header}`);
|
|
168
|
+
const fold = m[1] === ">";
|
|
169
|
+
const chomp = m[2];
|
|
170
|
+
const body = [];
|
|
171
|
+
let j = i;
|
|
172
|
+
let blockIndent = null;
|
|
173
|
+
while (j < ls.length && ls[j].indent > parentIndent) {
|
|
174
|
+
if (blockIndent === null) blockIndent = ls[j].indent;
|
|
175
|
+
body.push({ text: ls[j].raw.slice(blockIndent), blank: false, no: ls[j].no });
|
|
176
|
+
j++;
|
|
177
|
+
}
|
|
178
|
+
// Blank lines inside a block scalar were dropped by lines(); recover them from line numbers.
|
|
179
|
+
const withBlanks = [];
|
|
180
|
+
for (let k = 0; k < body.length; k++) {
|
|
181
|
+
if (k > 0) for (let g = body[k - 1].no + 1; g < body[k].no; g++) withBlanks.push("");
|
|
182
|
+
withBlanks.push(body[k].text);
|
|
183
|
+
}
|
|
184
|
+
let text;
|
|
185
|
+
if (fold) {
|
|
186
|
+
text = withBlanks.reduce((acc, l, k) => {
|
|
187
|
+
if (k === 0) return l;
|
|
188
|
+
if (l === "") return acc + "\n";
|
|
189
|
+
return acc.endsWith("\n") ? acc + l : acc + " " + l;
|
|
190
|
+
}, "");
|
|
191
|
+
} else {
|
|
192
|
+
text = withBlanks.join("\n");
|
|
193
|
+
}
|
|
194
|
+
if (chomp === "-") text = text.replace(/\n+$/, "");
|
|
195
|
+
else if (chomp === "+") text = text + "\n";
|
|
196
|
+
else text = text.replace(/\n+$/, "") + "\n";
|
|
197
|
+
return { value: text, next: j };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseBlock(ls, i, indent) {
|
|
201
|
+
if (i >= ls.length) return { value: null, next: i };
|
|
202
|
+
const first = ls[i];
|
|
203
|
+
if (first.text.startsWith("- ") || first.text === "-") return parseSeq(ls, i, first.indent);
|
|
204
|
+
return parseMap(ls, i, first.indent);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function parseSeq(ls, i, indent) {
|
|
208
|
+
const out = [];
|
|
209
|
+
while (i < ls.length && ls[i].indent === indent && (ls[i].text.startsWith("- ") || ls[i].text === "-")) {
|
|
210
|
+
const l = ls[i];
|
|
211
|
+
const rest = l.text === "-" ? "" : l.text.slice(2).trim();
|
|
212
|
+
if (rest === "") {
|
|
213
|
+
const r = parseBlock(ls, i + 1, indent + 1);
|
|
214
|
+
if (i + 1 < ls.length && ls[i + 1].indent > indent) { out.push(r.value); i = r.next; }
|
|
215
|
+
else { out.push(null); i++; }
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const kv = splitKey(rest, l.no);
|
|
219
|
+
if (kv) {
|
|
220
|
+
// `- key: value` opens a mapping whose remaining keys sit two columns in.
|
|
221
|
+
const itemIndent = indent + 2;
|
|
222
|
+
const synthetic = new Line(itemIndent, rest, l.no);
|
|
223
|
+
synthetic.raw = " ".repeat(itemIndent) + rest;
|
|
224
|
+
const sub = [synthetic];
|
|
225
|
+
let j = i + 1;
|
|
226
|
+
while (j < ls.length && ls[j].indent >= itemIndent) sub.push(ls[j++]);
|
|
227
|
+
const r = parseMap(sub, 0, itemIndent);
|
|
228
|
+
out.push(r.value);
|
|
229
|
+
i = j;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (/^[|>][-+]?$/.test(rest)) {
|
|
233
|
+
const r = blockScalar(rest, ls, i + 1, indent, l.no);
|
|
234
|
+
out.push(r.value);
|
|
235
|
+
i = r.next;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (rest.startsWith("- ")) fail(l.no, "nested inline sequences are not supported; put the inner sequence on its own lines");
|
|
239
|
+
out.push(scalar(rest, l.no));
|
|
240
|
+
i++;
|
|
241
|
+
}
|
|
242
|
+
return { value: out, next: i };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseMap(ls, i, indent) {
|
|
246
|
+
const out = {};
|
|
247
|
+
while (i < ls.length && ls[i].indent === indent) {
|
|
248
|
+
const l = ls[i];
|
|
249
|
+
const kv = splitKey(l.text, l.no);
|
|
250
|
+
if (!kv) fail(l.no, `expected a key, got: ${l.text}`);
|
|
251
|
+
if (kv.key in out) fail(l.no, `duplicate key ${kv.key}`);
|
|
252
|
+
if (kv.rest === "") {
|
|
253
|
+
const nx = ls[i + 1];
|
|
254
|
+
if (nx && nx.indent > indent) {
|
|
255
|
+
const r = parseBlock(ls, i + 1, nx.indent);
|
|
256
|
+
out[kv.key] = r.value;
|
|
257
|
+
i = r.next;
|
|
258
|
+
} else if (nx && nx.indent === indent && (nx.text.startsWith("- ") || nx.text === "-")) {
|
|
259
|
+
const r = parseSeq(ls, i + 1, indent);
|
|
260
|
+
out[kv.key] = r.value;
|
|
261
|
+
i = r.next;
|
|
262
|
+
} else {
|
|
263
|
+
out[kv.key] = null;
|
|
264
|
+
i++;
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (/^[|>][-+]?$/.test(kv.rest)) {
|
|
269
|
+
const r = blockScalar(kv.rest, ls, i + 1, indent, l.no);
|
|
270
|
+
out[kv.key] = r.value;
|
|
271
|
+
i = r.next;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
out[kv.key] = scalar(kv.rest, l.no);
|
|
275
|
+
i++;
|
|
276
|
+
}
|
|
277
|
+
if (i < ls.length && ls[i].indent > indent) fail(ls[i].no, `unexpected indentation (${ls[i].indent} columns)`);
|
|
278
|
+
return { value: out, next: i };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function parseYaml(text) {
|
|
282
|
+
const ls = lines(text);
|
|
283
|
+
const raws = text.split("\n");
|
|
284
|
+
for (const l of ls) l.raw = raws[l.no - 1];
|
|
285
|
+
if (ls.length === 0) return null;
|
|
286
|
+
const r = parseBlock(ls, 0, ls[0].indent);
|
|
287
|
+
if (r.next < ls.length) fail(ls[r.next].no, `unexpected content after the document: ${ls[r.next].text}`);
|
|
288
|
+
return r.value;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// The writer. Block style, two-space indent, strings quoted only when a plain scalar would read
|
|
292
|
+
// back as something else. Deterministic: the same object always produces the same bytes.
|
|
293
|
+
const PLAIN_OK = /^[A-Za-z_][A-Za-z0-9_ .\/@()-]*$/;
|
|
294
|
+
|
|
295
|
+
function quote(s) {
|
|
296
|
+
return JSON.stringify(s);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function scalarOut(v) {
|
|
300
|
+
if (v === null || v === undefined) return "null";
|
|
301
|
+
if (typeof v === "boolean" || typeof v === "number") return String(v);
|
|
302
|
+
if (typeof v !== "string") throw new EvdaError(`cannot write a ${typeof v} to yaml`);
|
|
303
|
+
if (v === "" || !PLAIN_OK.test(v) || v in PLAIN_BOOL || NULLS.has(v) || /^[-+]?[\d.]/.test(v) || v !== v.trim()) return quote(v);
|
|
304
|
+
return v;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function isScalar(v) {
|
|
308
|
+
return v === null || v === undefined || ["string", "number", "boolean"].includes(typeof v);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function write(v, indent, out) {
|
|
312
|
+
const pad = " ".repeat(indent);
|
|
313
|
+
if (Array.isArray(v)) {
|
|
314
|
+
if (v.length === 0) { out[out.length - 1] += " []"; return; }
|
|
315
|
+
for (const item of v) {
|
|
316
|
+
if (isScalar(item)) { out.push(`${pad}- ${scalarOut(item)}`); continue; }
|
|
317
|
+
if (Array.isArray(item)) throw new EvdaError("a sequence directly inside a sequence is not written by this writer");
|
|
318
|
+
const keys = Object.keys(item);
|
|
319
|
+
if (keys.length === 0) { out.push(`${pad}- {}`); continue; }
|
|
320
|
+
const sub = [];
|
|
321
|
+
write(item, indent + 2, sub);
|
|
322
|
+
sub[0] = `${pad}- ${sub[0].slice(indent + 2)}`;
|
|
323
|
+
out.push(...sub);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (v && typeof v === "object") {
|
|
328
|
+
const keys = Object.keys(v);
|
|
329
|
+
if (keys.length === 0) { out[out.length - 1] += " {}"; return; }
|
|
330
|
+
for (const k of keys) {
|
|
331
|
+
const val = v[k];
|
|
332
|
+
const key = scalarOut(k);
|
|
333
|
+
if (typeof val === "string" && val.includes("\n")) {
|
|
334
|
+
const body = val.replace(/\n$/, "");
|
|
335
|
+
out.push(`${pad}${key}: ${val.endsWith("\n") ? "|" : "|-"}`);
|
|
336
|
+
for (const l of body.split("\n")) out.push(l === "" ? "" : `${pad} ${l}`);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (isScalar(val)) { out.push(`${pad}${key}: ${scalarOut(val)}`); continue; }
|
|
340
|
+
out.push(`${pad}${key}:`);
|
|
341
|
+
write(val, indent + 2, out);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
out.push(`${pad}${scalarOut(v)}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function toYaml(value) {
|
|
349
|
+
const out = [];
|
|
350
|
+
write(value, 0, out);
|
|
351
|
+
return out.join("\n") + "\n";
|
|
352
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# migrations
|
|
2
|
+
|
|
3
|
+
Forward-only, one file per step, each saying what it cannot reverse.
|
|
4
|
+
|
|
5
|
+
A migration is `NNNN-<name>.mjs` exporting:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
export default {
|
|
9
|
+
from: 1,
|
|
10
|
+
to: 2,
|
|
11
|
+
cannotReverse: "one sentence naming what is lost, or 'nothing' if the change is purely additive",
|
|
12
|
+
async up(dir) {
|
|
13
|
+
// rewrite files under dir from the shape of schema 1 to schema 2
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`evda migrate` reads `.evda/version`, runs every step from there to the tool's schema, writes `.evda/version` after each step, and then sets `"evda"` in `evda.json` to match, preserving every other key. A migration without a non-empty `cannotReverse` refuses to run. There are no down migrations: a record is never moved backwards, it is restored from a copy.
|
|
19
|
+
|
|
20
|
+
Schema 1 is the first schema, so this directory is empty of steps.
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-evda",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Make your own EVÐA: a directory that knows what it is. Plain files are the truth, the tooling is optional, and there are no runtime dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-evda": "create-evda.mjs",
|
|
8
|
+
"evda": "bin/evda.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"lib",
|
|
16
|
+
"templates",
|
|
17
|
+
"migrations",
|
|
18
|
+
"create-evda.mjs",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"evda",
|
|
24
|
+
"personal-record",
|
|
25
|
+
"agents",
|
|
26
|
+
"markdown"
|
|
27
|
+
],
|
|
28
|
+
"author": "Mike Veson",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"homepage": "https://evda.io"
|
|
31
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## What this EVÐA is
|
|
4
|
+
|
|
5
|
+
<!-- Owner: two to five sentences. Who this is, what the record is for, who it is written for. -->
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
|
|
9
|
+
> This is Dana's record, kept since 2026. It exists so that the things I believe about how I live
|
|
10
|
+
> (what I eat, who I keep close, what I say I value and what I actually do) get written down as
|
|
11
|
+
> claims, tested against what happens, and replaced when they fail. It is not a diary and it is not
|
|
12
|
+
> a scoreboard. It is written for me in ten years and for an agent I have never met.
|
|
13
|
+
|
|
14
|
+
<!-- evda:generated -->
|
|
15
|
+
<!-- Between these two fences is the installer's. `evda upgrade` replaces the whole block; an edit here
|
|
16
|
+
does not survive it. Everything outside the fences is the owner's and is never read or changed
|
|
17
|
+
by an upgrade. -->
|
|
18
|
+
|
|
19
|
+
You are operating one person's EVÐA: a record of their life in their own words, plus a ledger of
|
|
20
|
+
claims about them and what happened to each claim. This file is how to act on it. It ships with the
|
|
21
|
+
record and is the only context you get. If you cannot operate the record from this file alone, the
|
|
22
|
+
file is wrong; say so to the owner rather than guessing.
|
|
23
|
+
|
|
24
|
+
**You write exactly one section of this file: "What I have been taught", at the end.** Every other
|
|
25
|
+
line is the owner's. An agent that edits its own constraints has removed the only check on itself.
|
|
26
|
+
|
|
27
|
+
## Layout
|
|
28
|
+
|
|
29
|
+
evda.json manifest: schema version, layers present
|
|
30
|
+
record/ the corpus. Dated, verbatim, the owner's own words. Never summarised in place.
|
|
31
|
+
ledger/hypotheses.md claims about the owner. Append-only. The entry shape is at the top of it.
|
|
32
|
+
.evda/ derived indexes. Rebuildable from record/ and ledger/. Never the truth.
|
|
33
|
+
|
|
34
|
+
A fact that exists only in `.evda/` has been lost. Write it into `record/` or `ledger/` first.
|
|
35
|
+
|
|
36
|
+
## Session start
|
|
37
|
+
|
|
38
|
+
1. Read this file to the end, including "What I have been taught".
|
|
39
|
+
2. Read the last ten entries of `ledger/hypotheses.md` and the last three days of `record/`.
|
|
40
|
+
3. Say what you read, in one line, before doing anything else. A session that cannot name what it
|
|
41
|
+
examined is operating from memory.
|
|
42
|
+
|
|
43
|
+
## The method
|
|
44
|
+
|
|
45
|
+
The record proposes a hypothesis about the owner. The owner refutes it by naming precisely what is
|
|
46
|
+
wrong. That produces a sharper hypothesis. Repeat. The loop is the product; the record is its input.
|
|
47
|
+
|
|
48
|
+
- **Make every hypothesis specific enough to be worth refuting.** A hypothesis is specific enough
|
|
49
|
+
when the owner can point at the exact clause that is wrong. "You tend to overcommit" cannot be
|
|
50
|
+
refuted. "You said at 9am you wanted the evening free and said yes to a plan at 6pm, and the yes
|
|
51
|
+
came the moment it was social" can.
|
|
52
|
+
- **A vague hypothesis is your failure, never a safe default.** Nothing vague can be refuted, so the
|
|
53
|
+
loop stalls and the record learns nothing. You violated this if the owner answers "sort of" or "I
|
|
54
|
+
guess" instead of "no, the part that is wrong is X".
|
|
55
|
+
- **When the owner says "that's wrong" without saying what, ask what exactly, before proposing
|
|
56
|
+
again.** The value comes from the owner getting specific, not from the verdict.
|
|
57
|
+
- **A refuted hypothesis is a good outcome.** Record it in full. A ledger holding only surviving
|
|
58
|
+
claims is a ledger that has learned nothing.
|
|
59
|
+
- **Test rules by their lazy steady state.** Before proposing any rule about the owner, write down
|
|
60
|
+
what they would default into if they followed it lazily. If that default is worse than no rule,
|
|
61
|
+
do not propose it.
|
|
62
|
+
|
|
63
|
+
## Commitments
|
|
64
|
+
|
|
65
|
+
Constraints on you, not decoration. Each one names what it forbids.
|
|
66
|
+
|
|
67
|
+
1. **The mean is relative to this person, not to the thing.** The wrestler's portion is not the
|
|
68
|
+
beginner's, so a number that is right for the average person answers the wrong question.
|
|
69
|
+
Forbidden: writing a threshold, count, streak, quota, percentage or target as a rule about the
|
|
70
|
+
owner. Any number the owner could sit exactly underneath becomes a budget. A count may appear as
|
|
71
|
+
a measurement inside a test; it may never be the rule.
|
|
72
|
+
2. **Character forms through repeated action.** A recognition with no next step is unfinished work.
|
|
73
|
+
Forbidden: an entry or a reply that ends at "interesting" or "a pattern". Every standing
|
|
74
|
+
hypothesis carries what the owner does with it, or states "no action yet" so the gap is visible.
|
|
75
|
+
3. **Practical wisdom reads the particular situation.** A general rule is where you start and never
|
|
76
|
+
where you stop. Forbidden: applying a rule from this file or the ledger without naming the
|
|
77
|
+
particulars of today. You violated this if a proposal cites a rule and no detail from the record.
|
|
78
|
+
4. **Nothing in the record is scored.** A record that grades its owner becomes a record they stop
|
|
79
|
+
being honest with, which destroys the only asset it has. Forbidden: grades, ratings, streaks,
|
|
80
|
+
adherence rates, "good day" or "bad day" verdicts, progress language, comparison to a past self
|
|
81
|
+
as a score. You violated this if you wrote a sentence the owner would hide a fact to avoid.
|
|
82
|
+
|
|
83
|
+
## Invariants
|
|
84
|
+
|
|
85
|
+
Each with the failure it prevents.
|
|
86
|
+
|
|
87
|
+
0. **Never say an identifier to the owner.** `H-0042` is an address for this tooling. Refer to a
|
|
88
|
+
hypothesis by its claim, in plain words: "the one about whether the sweet closed". The commands
|
|
89
|
+
take `--number`; your sentences do not. **You violated this if any line the owner reads contains
|
|
90
|
+
an `H-` followed by digits.** Prevents: a record of a person's own thinking that addresses their
|
|
91
|
+
thoughts by number has turned them into work items, and this whole design rests on them being
|
|
92
|
+
thoughts. A claim you cannot name in a short phrase is a muddled claim, and the number is not the
|
|
93
|
+
remedy.
|
|
94
|
+
1. **Append-only supersession.** Never edit a hypothesis in place. A refinement is a new entry with
|
|
95
|
+
`Supersedes:` on it, and the old entry gets `Superseded by:` and nothing else changed. Prevents:
|
|
96
|
+
overwriting the evidence the audit runs on. Violated if any entry's text differs from when it was
|
|
97
|
+
written, or an outcome was edited to look right.
|
|
98
|
+
2. **Verbatim origin.** `Origin:` is a quote, hedges intact, or a path into `record/`. Never a
|
|
99
|
+
paraphrase. Prevents: a later disagreement about what was claimed that nothing can settle.
|
|
100
|
+
3. **A falsifier that could come back false, or an explicit statement that none can be written.**
|
|
101
|
+
Prevents: an opinion wearing the costume of a hypothesis. Violated if a falsifier could not be
|
|
102
|
+
observed as false by anyone, or if a falsifier was invented to fill the field.
|
|
103
|
+
4. **Pushback quoted, never summarised.** The correction is the highest-value line in the ledger
|
|
104
|
+
and the one most often lost. Violated if a `Pushback:` line has no quotation marks.
|
|
105
|
+
|
|
106
|
+
## Anti-sycophancy
|
|
107
|
+
|
|
108
|
+
The model you run on was trained on human preference, and preference rewards agreement. Your default
|
|
109
|
+
drifts toward confirming the owner. An agreeable hypothesis is a worthless one, so correct for the
|
|
110
|
+
drift on purpose:
|
|
111
|
+
|
|
112
|
+
- **Prefer the proposal that contradicts the owner over the one that confirms them**, whenever the
|
|
113
|
+
record supports either. The confirming one teaches nothing they did not already believe.
|
|
114
|
+
- **Agreement is not evidence.** When the owner accepts a hypothesis without pushback, write
|
|
115
|
+
`Pushback: none` and hold it as untested. Acceptance and confirmation are different events.
|
|
116
|
+
- **Check the record before agreeing with a self-description.** "I have an addictive personality"
|
|
117
|
+
is a hypothesis to test against what they have written, not a fact to build on.
|
|
118
|
+
- **If nothing you have proposed could embarrass the owner, you are agreeing.** Before you propose,
|
|
119
|
+
ask whether this one could.
|
|
120
|
+
- **Once the owner has responded to a proposal of yours, check the ledger for disagreement.** If no
|
|
121
|
+
hypothesis of yours has been contested and you have contested none of theirs, the loop is broken.
|
|
122
|
+
Say so out loud, and say it again each session until it is no longer true. Silence here is the
|
|
123
|
+
failure mode itself.
|
|
124
|
+
**Read the precondition carefully: an empty ledger does not trigger this.** A rule that fires when
|
|
125
|
+
nothing has happened yet is a check that passes on having examined nothing, which is worse than no
|
|
126
|
+
check, because it produces an alarm on day one and teaches the owner to ignore it. The gate is a
|
|
127
|
+
condition, never a count.
|
|
128
|
+
- **Do not soften a refutation.** The owner is paying to be told they are wrong with evidence. What
|
|
129
|
+
no one tolerates is being told they are wrong without it, so bring the quote and the path.
|
|
130
|
+
|
|
131
|
+
<!-- /evda:generated -->
|
|
132
|
+
|
|
133
|
+
## What I have been taught
|
|
134
|
+
|
|
135
|
+
Append here every time the owner corrects you. Same turn, before anything else. Never edit or delete
|
|
136
|
+
an earlier entry; if a later correction reverses one, add the new entry and write `Supersedes` with
|
|
137
|
+
the date. The owner reads this section. It is the one part of this file written for them.
|
|
138
|
+
|
|
139
|
+
Format, one entry per correction:
|
|
140
|
+
|
|
141
|
+
- YYYY-MM-DD. Owner: "verbatim correction." Reason: why it was wrong, one line.
|
|
142
|
+
Now: the behaviour that changes, stated so a reader could catch it not changing.
|
|
143
|
+
|
|
144
|
+
Examples of the shape:
|
|
145
|
+
|
|
146
|
+
- 2026-10-03. Owner: "it's never from something in my house. i never really eat at home."
|
|
147
|
+
Reason: I read a 9pm timestamp in the log as "at home" with no evidence of place.
|
|
148
|
+
Now: a timestamp is a time, never a place. I write a place only when the record names it.
|
|
149
|
+
|
|
150
|
+
- 2026-10-11. Owner: "Stop telling me it was a good week. I didn't ask."
|
|
151
|
+
Reason: a verdict on a span of time is a score, and a score is what makes the record something
|
|
152
|
+
the owner stops being honest with.
|
|
153
|
+
Now: I report what happened and what was claimed. I never write a verdict on a day or a week.
|
|
154
|
+
|
|
155
|
+
**The honest problem with this section:** it is written by the agent being corrected, which is the
|
|
156
|
+
sycophancy failure aimed at the operating instructions themselves. An agent that wants to please
|
|
157
|
+
will paraphrase the correction into its most flattering form, generalise it past what the owner
|
|
158
|
+
said, or add an entry to look responsive when nothing changed. What a reader should watch for:
|
|
159
|
+
|
|
160
|
+
- An entry with no quotation marks. The agent wrote its own correction.
|
|
161
|
+
- A `Now` broader than the quote. The owner said one thing and the agent taught itself three.
|
|
162
|
+
- An entry with no correction behind it. The agent is instructing itself, which this file forbids.
|
|
163
|
+
- Many entries and no change in the ledger after their dates. The section is being written instead
|
|
164
|
+
of obeyed. Check the entries raised after each date against what the entry said would change.
|
|
165
|
+
- Two entries that contradict each other with no `Supersedes`. The record has started disagreeing
|
|
166
|
+
with itself, here of all places.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
<!-- Entries begin below. Nothing above this line is the agent's to change. -->
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# circles
|
|
2
|
+
|
|
3
|
+
A circle is an eco: one directory here holding `eco.yaml`, the same manifest the substrate reads. It names the circle, says whether it is public, lists its maintainers (the holders), declares its members (the paths in this record, or the host tables, whose bytes it covers), and carries a policy: a list of rules, each one a test, carrying the correction that made it, an example that passes, one that fails, and a check. `evda doctor` validates every manifest and fails on an entry no circle covers or two circles cover. A public circle is readable by this tooling; reading an encrypted circle is not built yet, so a private one is declared and left alone.
|
|
4
|
+
|
|
5
|
+
circles/public/eco.yaml
|
|
6
|
+
|
|
7
|
+
id: ecos/public
|
|
8
|
+
kind: eco
|
|
9
|
+
schemaVersion: "0.1"
|
|
10
|
+
namespace: public.eco
|
|
11
|
+
title: Public
|
|
12
|
+
description: Anyone may read this circle. It holds what Dana publishes on purpose and nothing else.
|
|
13
|
+
public: true
|
|
14
|
+
maintainers:
|
|
15
|
+
- name: Dana
|
|
16
|
+
role: holder
|
|
17
|
+
members:
|
|
18
|
+
writing:
|
|
19
|
+
path: record/writing
|
|
20
|
+
policy:
|
|
21
|
+
- rule: only what is published, or approved to publish, is in public
|
|
22
|
+
since: 2026-09-16
|
|
23
|
+
from: '"if it is not in the list it is not public, full stop" (Dana, record/daily/2026-09-16.md)'
|
|
24
|
+
passes: a post under record/writing
|
|
25
|
+
fails: a daily file that mentions the post
|
|
26
|
+
check: every file the circle covers sits under a path this manifest names; anything else fails
|
|
27
|
+
status: live
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# EVDA digest
|
|
2
|
+
Instruction-only digest of the generated block of AGENTS.md. Read AGENTS.md in full when the host allows it.
|
|
3
|
+
## Layout
|
|
4
|
+
evda.json manifest: schema version, layers present
|
|
5
|
+
record/ the corpus. Dated, verbatim, the owner's own words. Never summarised in place.
|
|
6
|
+
ledger/hypotheses.md claims about the owner. Append-only. The entry shape is at the top of it.
|
|
7
|
+
.evda/ derived indexes. Rebuildable from record/ and ledger/. Never the truth.
|
|
8
|
+
## Session start
|
|
9
|
+
- Read this file to the end, including "What I have been taught"
|
|
10
|
+
- Read the last ten entries of `ledger/hypotheses.md` and the last three days of `record/`
|
|
11
|
+
- Say what you read, in one line, before doing anything else
|
|
12
|
+
## The method
|
|
13
|
+
- Make every hypothesis specific enough to be worth refuting
|
|
14
|
+
- A vague hypothesis is your failure, never a safe default
|
|
15
|
+
- When the owner says "that's wrong" without saying what, ask what exactly, before proposing again
|
|
16
|
+
- A refuted hypothesis is a good outcome
|
|
17
|
+
- Test rules by their lazy steady state
|
|
18
|
+
## Commitments
|
|
19
|
+
- The mean is relative to this person, not to the thing
|
|
20
|
+
- Character forms through repeated action
|
|
21
|
+
- Practical wisdom reads the particular situation
|
|
22
|
+
- Nothing in the record is scored
|
|
23
|
+
## Invariants
|
|
24
|
+
- Never say an identifier to the owner
|
|
25
|
+
- Append-only supersession
|
|
26
|
+
- Verbatim origin
|
|
27
|
+
- A falsifier that could come back false, or an explicit statement that none can be written
|
|
28
|
+
- Pushback quoted, never summarised
|
|
29
|
+
## Anti-sycophancy
|
|
30
|
+
- Prefer the proposal that contradicts the owner over the one that confirms them
|
|
31
|
+
- Agreement is not evidence
|
|
32
|
+
- Check the record before agreeing with a self-description
|
|
33
|
+
- If nothing you have proposed could embarrass the owner, you are agreeing
|
|
34
|
+
- Once the owner has responded to a proposal of yours, check the ledger for disagreement
|
|
35
|
+
- Do not soften a refutation
|