create-evda 0.1.4 → 0.1.6

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +93 -8
  3. package/bin/evda.mjs +437 -5
  4. package/create-evda.mjs +68 -12
  5. package/docs/local-model.md +34 -0
  6. package/hooks/evda-read-guard.mjs +7 -0
  7. package/lib/chat-export.mjs +198 -0
  8. package/lib/circle-plan.mjs +277 -0
  9. package/lib/circles.mjs +479 -28
  10. package/lib/connect.mjs +46 -0
  11. package/lib/day-file.mjs +93 -0
  12. package/lib/declarations.mjs +235 -0
  13. package/lib/devices.mjs +202 -0
  14. package/lib/doctor.mjs +351 -19
  15. package/lib/filechanges.mjs +622 -0
  16. package/lib/folders.mjs +22 -8
  17. package/lib/gather-apply.mjs +635 -0
  18. package/lib/gather.mjs +808 -0
  19. package/lib/grants.mjs +7 -1
  20. package/lib/guard.mjs +132 -0
  21. package/lib/hooks.mjs +124 -0
  22. package/lib/keep-hook.mjs +185 -0
  23. package/lib/kinds.mjs +23 -6
  24. package/lib/kinds.schema.json +2 -1
  25. package/lib/manifest.mjs +14 -1
  26. package/lib/mcp-auth.mjs +370 -0
  27. package/lib/mcp-http.mjs +336 -0
  28. package/lib/mcp.mjs +467 -35
  29. package/lib/migrate-people.mjs +231 -0
  30. package/lib/organize.mjs +52 -12
  31. package/lib/parity.mjs +111 -0
  32. package/lib/presets.mjs +379 -0
  33. package/lib/prompt-hook.mjs +88 -0
  34. package/lib/publish.mjs +264 -0
  35. package/lib/readers.mjs +319 -0
  36. package/lib/records.mjs +52 -0
  37. package/lib/references.mjs +5 -51
  38. package/lib/render.mjs +158 -0
  39. package/lib/roles.mjs +102 -1
  40. package/lib/secret-shape.mjs +58 -0
  41. package/lib/setup.mjs +146 -2
  42. package/lib/skills.mjs +266 -0
  43. package/lib/subrecord.mjs +121 -0
  44. package/lib/sync-rules.mjs +87 -0
  45. package/lib/tools.mjs +120 -29
  46. package/lib/update-check.mjs +146 -0
  47. package/lib/work.mjs +180 -0
  48. package/lib/yaml.mjs +117 -42
  49. package/package.json +25 -15
  50. package/presets/journal/preset.json +19 -0
  51. package/presets/projects-and-areas/preset.json +34 -0
  52. package/skills/evda/SKILL.md +76 -0
  53. package/templates/AGENTS.md +129 -15
  54. package/templates/README.md +15 -1
  55. package/templates/circles-README.md +3 -3
  56. package/templates/evda-digest.md +32 -5
  57. package/LICENSE +0 -21
package/lib/yaml.mjs CHANGED
@@ -2,7 +2,8 @@
2
2
  //
3
3
  // The substrate writes an eco as `eco.yaml`, and this package has no runtime dependencies, so the
4
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.
5
+ // sequences, plain and quoted scalars, block scalars (`|`, `>`, with chomping and indentation
6
+ // indicators), and one level of flow collections.
6
7
  // Anchors, aliases, tags, multi-document streams and tab indentation are refused with a line
7
8
  // number rather than guessed at. `test/yaml.test.mjs` checks this reader against the `yaml`
8
9
  // package over the substrate's own manifests, so the two agree on every file that matters.
@@ -48,6 +49,13 @@ function lines(text) {
48
49
  return out;
49
50
  }
50
51
 
52
+ // Set a parsed key as an own property. A plain `out[key] = v` treats "__proto__" as the prototype,
53
+ // and `key in out` finds "toString" and "constructor" on every object, so a frontmatter key named
54
+ // like an Object method read as a duplicate (HQ-1406).
55
+ function put(out, key, value) {
56
+ Object.defineProperty(out, key, { value, enumerable: true, writable: true, configurable: true });
57
+ }
58
+
51
59
  const PLAIN_BOOL = { true: true, false: false, True: true, False: false, TRUE: true, FALSE: false };
52
60
  const NULLS = new Set(["null", "Null", "NULL", "~", ""]);
53
61
 
@@ -125,7 +133,7 @@ function flow(t, no) {
125
133
  skip();
126
134
  if (peek() !== ":") fail(no, "expected : in flow mapping");
127
135
  i++;
128
- out[String(k)] = value();
136
+ put(out, String(k), value(), no);
129
137
  skip();
130
138
  if (peek() === ",") { i++; continue; }
131
139
  if (peek() === "}") { i++; return out; }
@@ -147,8 +155,10 @@ function splitKey(text, no) {
147
155
  let key;
148
156
  if (text[0] === '"' || text[0] === "'") {
149
157
  const q = text[0];
158
+ // An escaped quote inside a double-quoted key does not end it: the writer quotes a key with
159
+ // JSON.stringify, so `"a\"b"` is one key (HQ-1406).
150
160
  let j = 1;
151
- while (j < text.length && text[j] !== q) j++;
161
+ while (j < text.length && text[j] !== q) j += q === '"' && text[j] === "\\" ? 2 : 1;
152
162
  if (j >= text.length) fail(no, "unterminated quoted key");
153
163
  key = scalar(text.slice(0, j + 1), no);
154
164
  i = j + 1;
@@ -162,39 +172,92 @@ function splitKey(text, no) {
162
172
  return { key: String(key), rest: text.slice(i + 1).trim() };
163
173
  }
164
174
 
165
- function blockScalar(header, ls, i, parentIndent, no) {
166
- const m = /^([|>])([-+]?)$/.exec(header);
175
+ // A block scalar (`|` or `>`) is read from the raw text lines, not from lines(): its body keeps
176
+ // what lines() drops, a `#` line, a leading or trailing blank line, a whitespace-only line (HQ-1409).
177
+ // YAML 1.2 rules: the indentation is the header's indicator over the parent's, or else the first
178
+ // non-blank line's; the body ends at the first non-blank line indented less; `>` folds a line break
179
+ // between two lines that do not start with whitespace; `-` strips the final newlines, `+` keeps
180
+ // them all, and the default keeps one. Where the spec leaves a corner open (a whitespace-only line
181
+ // at the end, a text with no final line break) this reads it as the `yaml` package does, since
182
+ // every other tool that reads these files uses it. `src` is the text's lines, `no` the header's.
183
+ function blockScalar(header, ls, i, parentIndent, no, src) {
184
+ const m = /^([|>])(?:([-+]?)([1-9]?)|([1-9])([-+]))$/.exec(header);
167
185
  if (!m) fail(no, `unsupported block scalar header ${header}`);
168
186
  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++;
187
+ const chomp = m[2] || m[5] || "";
188
+ const indicator = Number(m[3] || m[4] || 0);
189
+ // The text's final line break ends its last line; it does not open another one.
190
+ const last = src.length > 0 && src[src.length - 1].replace(/\r$/, "") === "" ? src.length - 1 : src.length;
191
+ const raw = (k) => src[k].replace(/\r$/, "");
192
+ const spaces = (l) => l.length - l.replace(/^ +/, "").length;
193
+ const blank = (l) => /^ *$/.test(l);
194
+ let first = null; // the first non-blank line's indentation
195
+ for (let k = no; k < last; k++) {
196
+ const l = raw(k);
197
+ if (blank(l)) continue;
198
+ if (spaces(l) > parentIndent) first = spaces(l);
199
+ break;
200
+ }
201
+ // With no content line, every line up to the next less-indented one is blank, however long.
202
+ const indent = indicator ? parentIndent + indicator : first ?? Infinity;
203
+ const lines = []; // each line: its text past the indentation, or null when blank
204
+ let k = no;
205
+ for (; k < last; k++) {
206
+ const l = raw(k);
207
+ if (blank(l)) { lines.push({ text: l.length > indent ? l.slice(indent) : null, len: l.length }); continue; }
208
+ if (spaces(l) < indent) {
209
+ if (/^ *\t/.test(l)) fail(k + 1, "tab indentation");
210
+ break;
211
+ }
212
+ lines.push({ text: l.slice(indent), len: l.length });
213
+ }
214
+ const open = k === last && last === src.length; // the block ends the text, with no final line break
215
+ const hasContent = lines.some((l) => l.text !== null && !blank(l.text));
216
+ if (!hasContent) {
217
+ if (chomp !== "+" || lines.length === 0) return done("");
218
+ return done("\n".repeat(Math.max(1, lines.length - (open ? 1 : 0))));
219
+ }
220
+ // YAML refuses a leading blank line indented past the block, since it would decide the indentation.
221
+ if (!indicator) {
222
+ for (let b = 0; b < lines.length && (lines[b].text === null || blank(lines[b].text)); b++) {
223
+ if (lines[b].text !== null) fail(no + b + 1, "a leading all-space line is indented past the block; give the header an indentation indicator");
224
+ }
177
225
  }
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);
226
+ // Without `+`, a whitespace-only last line no longer than the first content line is not content.
227
+ let end = lines.length;
228
+ for (;;) {
229
+ while (end > 0 && lines[end - 1].text === null) end--;
230
+ if (chomp !== "+" && end > 0 && blank(lines[end - 1].text) && lines[end - 1].len <= first) { lines[--end].text = null; continue; }
231
+ break;
183
232
  }
184
- let text;
233
+ const content = lines.slice(0, end).map((l) => l.text ?? "");
234
+ let text = "";
185
235
  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
- }, "");
236
+ const moreIndented = (l) => /^[ \t]/.test(l);
237
+ let prev = null;
238
+ let empties = 0;
239
+ for (const l of content) {
240
+ if (l === "") { empties++; continue; }
241
+ if (prev === null) text += "\n".repeat(empties);
242
+ else if (!moreIndented(prev) && !moreIndented(l)) text += empties === 0 ? " " : "\n".repeat(empties);
243
+ else text += "\n".repeat(empties + 1);
244
+ text += l;
245
+ prev = l;
246
+ empties = 0;
247
+ }
191
248
  } else {
192
- text = withBlanks.join("\n");
249
+ text = content.join("\n");
250
+ }
251
+ if (chomp === "-") return done(text);
252
+ if (chomp === "") return done(text + "\n");
253
+ // `+` keeps the content line's break and one per blank line after it that ends in a line break.
254
+ return done(text + "\n" + "\n".repeat(Math.max(0, lines.length - end - (open ? 1 : 0))));
255
+
256
+ function done(value) {
257
+ let j = i;
258
+ while (j < ls.length && ls[j].no <= k) j++;
259
+ return { value, next: j };
193
260
  }
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
261
  }
199
262
 
200
263
  function parseBlock(ls, i, indent) {
@@ -220,7 +283,7 @@ function parseSeq(ls, i, indent) {
220
283
  // `- key: value` opens a mapping whose remaining keys sit two columns in.
221
284
  const itemIndent = indent + 2;
222
285
  const synthetic = new Line(itemIndent, rest, l.no);
223
- synthetic.raw = " ".repeat(itemIndent) + rest;
286
+ synthetic.src = l.src;
224
287
  const sub = [synthetic];
225
288
  let j = i + 1;
226
289
  while (j < ls.length && ls[j].indent >= itemIndent) sub.push(ls[j++]);
@@ -229,8 +292,8 @@ function parseSeq(ls, i, indent) {
229
292
  i = j;
230
293
  continue;
231
294
  }
232
- if (/^[|>][-+]?$/.test(rest)) {
233
- const r = blockScalar(rest, ls, i + 1, indent, l.no);
295
+ if (/^[|>][-+1-9]*$/.test(rest)) {
296
+ const r = blockScalar(rest, ls, i + 1, indent, l.no, l.src);
234
297
  out.push(r.value);
235
298
  i = r.next;
236
299
  continue;
@@ -248,30 +311,30 @@ function parseMap(ls, i, indent) {
248
311
  const l = ls[i];
249
312
  const kv = splitKey(l.text, l.no);
250
313
  if (!kv) fail(l.no, `expected a key, got: ${l.text}`);
251
- if (kv.key in out) fail(l.no, `duplicate key ${kv.key}`);
314
+ if (Object.hasOwn(out, kv.key)) fail(l.no, `duplicate key ${kv.key}`);
252
315
  if (kv.rest === "") {
253
316
  const nx = ls[i + 1];
254
317
  if (nx && nx.indent > indent) {
255
318
  const r = parseBlock(ls, i + 1, nx.indent);
256
- out[kv.key] = r.value;
319
+ put(out, kv.key, r.value);
257
320
  i = r.next;
258
321
  } else if (nx && nx.indent === indent && (nx.text.startsWith("- ") || nx.text === "-")) {
259
322
  const r = parseSeq(ls, i + 1, indent);
260
- out[kv.key] = r.value;
323
+ put(out, kv.key, r.value);
261
324
  i = r.next;
262
325
  } else {
263
- out[kv.key] = null;
326
+ put(out, kv.key, null);
264
327
  i++;
265
328
  }
266
329
  continue;
267
330
  }
268
- if (/^[|>][-+]?$/.test(kv.rest)) {
269
- const r = blockScalar(kv.rest, ls, i + 1, indent, l.no);
270
- out[kv.key] = r.value;
331
+ if (/^[|>][-+1-9]*$/.test(kv.rest)) {
332
+ const r = blockScalar(kv.rest, ls, i + 1, indent, l.no, l.src);
333
+ put(out, kv.key, r.value);
271
334
  i = r.next;
272
335
  continue;
273
336
  }
274
- out[kv.key] = scalar(kv.rest, l.no);
337
+ put(out, kv.key, scalar(kv.rest, l.no));
275
338
  i++;
276
339
  }
277
340
  if (i < ls.length && ls[i].indent > indent) fail(ls[i].no, `unexpected indentation (${ls[i].indent} columns)`);
@@ -281,7 +344,7 @@ function parseMap(ls, i, indent) {
281
344
  export function parseYaml(text) {
282
345
  const ls = lines(text);
283
346
  const raws = text.split("\n");
284
- for (const l of ls) l.raw = raws[l.no - 1];
347
+ for (const l of ls) l.src = raws;
285
348
  if (ls.length === 0) return null;
286
349
  const r = parseBlock(ls, 0, ls[0].indent);
287
350
  if (r.next < ls.length) fail(ls[r.next].no, `unexpected content after the document: ${ls[r.next].text}`);
@@ -304,6 +367,18 @@ function scalarOut(v) {
304
367
  return v;
305
368
  }
306
369
 
370
+ // Whether a multi-line string is written as a `|` block. A string whose block would need a header
371
+ // indicator (a leading blank or indented first line, kept trailing newlines) or would carry a `#`,
372
+ // a carriage return or trailing whitespace is written double-quoted instead, which always reads
373
+ // back and reads the same in every YAML tool (HQ-1406: "\na" read back as "a").
374
+ function blockSafe(v) {
375
+ const body = v.endsWith("\n") ? v.slice(0, -1) : v;
376
+ if (body.endsWith("\n") || body.includes("\r") || body.includes("#")) return false;
377
+ const ls = body.split("\n");
378
+ if (ls[0] === "" || /^\s/.test(ls[0])) return false;
379
+ return ls.every((l) => !/\s$/.test(l));
380
+ }
381
+
307
382
  function isScalar(v) {
308
383
  return v === null || v === undefined || ["string", "number", "boolean"].includes(typeof v);
309
384
  }
@@ -330,7 +405,7 @@ function write(v, indent, out) {
330
405
  for (const k of keys) {
331
406
  const val = v[k];
332
407
  const key = scalarOut(k);
333
- if (typeof val === "string" && val.includes("\n")) {
408
+ if (typeof val === "string" && val.includes("\n") && blockSafe(val)) {
334
409
  const body = val.replace(/\n$/, "");
335
410
  out.push(`${pad}${key}: ${val.endsWith("\n") ? "|" : "|-"}`);
336
411
  for (const l of body.split("\n")) out.push(l === "" ? "" : `${pad} ${l}`);
package/package.json CHANGED
@@ -1,33 +1,43 @@
1
1
  {
2
2
  "name": "create-evda",
3
- "version": "0.1.4",
4
- "description": "Make your own EVÐA: a running journal of your life with Iris, as plain files on your machine, for any AI. Connect Claude Code, Codex or the Claude app over MCP. No runtime dependencies.",
3
+ "version": "0.1.6",
5
4
  "type": "module",
5
+ "description": "The person layer over the substrate: scaffold and operate an EVDA, a directory that knows what it is. Plain files are the truth, the tooling is optional, and there are no runtime dependencies.",
6
6
  "bin": {
7
- "create-evda": "create-evda.mjs",
8
- "evda": "bin/evda.mjs"
7
+ "create-evda": "./create-evda.mjs",
8
+ "evda": "./bin/evda.mjs"
9
9
  },
10
10
  "engines": {
11
11
  "node": ">=20"
12
12
  },
13
+ "scripts": {
14
+ "test": "node --test test/*.test.mjs",
15
+ "lint": "node scripts/dashes-check.mjs && node scripts/build-templates.mjs --check && node scripts/build-kinds.mjs --check",
16
+ "templates:build": "node scripts/build-templates.mjs",
17
+ "templates:check": "node scripts/build-templates.mjs --check",
18
+ "kinds:build": "node scripts/build-kinds.mjs",
19
+ "kinds:check": "node scripts/build-kinds.mjs --check",
20
+ "dashes:check": "node scripts/dashes-check.mjs",
21
+ "vectors:both": "bash scripts/vectors-both.sh",
22
+ "doctor": "node bin/evda.mjs doctor",
23
+ "publish:dry": "node scripts/publish.mjs",
24
+ "publish:live": "node scripts/publish.mjs --publish"
25
+ },
13
26
  "files": [
14
27
  "bin",
28
+ "hooks",
29
+ "docs",
15
30
  "lib",
16
31
  "templates",
17
32
  "skills",
18
33
  "migrations",
34
+ "presets",
19
35
  "create-evda.mjs",
20
36
  "README.md",
21
- "LICENSE"
22
- ],
23
- "keywords": [
24
- "evda",
25
- "personal-record",
26
- "agents",
27
- "markdown",
28
- "mcp"
37
+ "CHANGELOG.md"
29
38
  ],
30
- "author": "Mike Veson",
31
- "license": "MIT",
32
- "homepage": "https://evda.io"
39
+ "atlas": {
40
+ "tier": "block",
41
+ "domain": "neutral"
42
+ }
33
43
  }
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "journal",
3
+ "title": "Journal",
4
+ "for": "Someone who mostly wants to write about their days and the people in them, with little structure.",
5
+ "folders": [
6
+ { "path": "Journal", "type": "record", "note": "One file per day, named YYYY-MM-DD.md." },
7
+ { "path": "Captures", "type": "capture", "note": "A thought as it arrived." },
8
+ { "path": "Work", "type": "ticket", "note": "One file per thing being done, with its status and the day it is planned for." },
9
+ { "path": "People", "type": "person", "connectors": ["contacts"], "note": "One file per person." },
10
+ { "path": "Notes", "type": "concept", "note": "Ideas worth keeping past the day." }
11
+ ],
12
+ "mapping": {
13
+ "journal": "Journal",
14
+ "journal/captures": "Captures",
15
+ "work": "Work",
16
+ "collections": "People",
17
+ "knowledge": "Notes"
18
+ }
19
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "id": "projects-and-areas",
3
+ "title": "Projects and areas",
4
+ "for": "Someone running several projects and parts of life at once, courses included, who captures fast, sorts later, and looks back each week.",
5
+ "folders": [
6
+ { "path": "01_Projects", "type": "project", "note": "One folder per project; a finished one stays here with its status." },
7
+ { "path": "02_Areas", "note": "Parts of life with no end date: one folder per area." },
8
+ { "path": "02_Areas/Academics", "type": "course", "note": "One folder per course; a course is a collection like any other." },
9
+ { "path": "02_Areas/Relationships/People", "type": "person", "connectors": ["contacts"], "note": "One file per person." },
10
+ { "path": "03_Resources", "note": "What is known, each page citing where it came from." },
11
+ { "path": "03_Resources/Raw", "type": "source", "connectors": ["url"], "note": "Sources as they arrived, unedited." },
12
+ { "path": "03_Resources/Wiki/Concepts", "type": "concept", "note": "One page per recurring idea, named." },
13
+ { "path": "03_Resources/Wiki/Syntheses", "type": "concept", "note": "Connections worked out across ideas." },
14
+ { "path": "03_Resources/Wiki/Sources", "type": "source", "connectors": ["url"], "note": "A summary per source cited often." },
15
+ { "path": "04_Work", "type": "ticket", "note": "One file per effort, with its status and the day it is planned for; a board can take its place." },
16
+ { "path": "05_Process", "type": "capture", "note": "Captures waiting to be filed." },
17
+ { "path": "05_Process/Processed", "type": "capture", "note": "Captures already filed, kept." },
18
+ { "path": "05_Process/_media", "type": "blob", "note": "Photos and audio a capture carried." },
19
+ { "path": "06_Metadata/Daily Journal", "type": "record", "note": "One file per day, named YYYY-MM-DD.md." },
20
+ { "path": "06_Metadata/Weekly Reviews", "type": "concept", "note": "One look back per week." },
21
+ { "path": "07_Attachments", "type": "blob", "note": "Files notes link to." }
22
+ ],
23
+ "mapping": {
24
+ "journal": "06_Metadata/Daily Journal",
25
+ "journal/captures": "05_Process",
26
+ "work": "04_Work",
27
+ "collections": "01_Projects",
28
+ "collections/areas": "02_Areas",
29
+ "collections/attachments": "07_Attachments",
30
+ "knowledge": "03_Resources",
31
+ "knowledge/reviews": "06_Metadata/Weekly Reviews"
32
+ },
33
+ "routines": ["morning", "weekly"]
34
+ }
@@ -16,6 +16,12 @@ calls no model; you are their own AI, and everything you keep goes through the t
16
16
  Where it and this skill disagree, it wins.
17
17
  2. Call `evda_recent` for the last few days, and read what they point you at with `evda_search`.
18
18
  3. Say in one line what you read, then answer them.
19
+ 4. Call `evda_check_update` once (throttled to once an hour; most sessions read a cached result).
20
+ Say nothing if it is current or was skipped. If an update is available or the check failed, say
21
+ the one line it returns, briefly, and hand them the command it names to run in a terminal. The
22
+ tool only tells: nothing in this server installs an update. It asks the npm registry for a
23
+ version number and sends nothing from the record, and it is the only tool here that reaches the
24
+ network at all. `evda check-update` is the same check from a shell, for a session that has one.
19
25
 
20
26
  If the tools are not there, say so plainly and point them at `evda connect`, which prints the
21
27
  setup for each client. Do not keep the record some other way.
@@ -24,6 +30,20 @@ setup for each client. Do not keep the record some other way.
24
30
 
25
31
  - **File their words in the same turn they say them**, with `evda_write`: verbatim, filler, hedges
26
32
  and typos intact. Never a summary, never your words.
33
+ - **Without being asked.** They should never have to say "save this". When a tool result says
34
+ nothing from this conversation is filed yet, or a Stop hook asks before you finish, that is this
35
+ rule: file what they said that is worth keeping, then stop. When `evda.json` says
36
+ `"filing": "asked"`, they chose to file only when they ask; when it says nothing, ask them once.
37
+ - **One section per conversation.** Your first `evda_write` opens it with one line of `context`;
38
+ later ones go under it.
39
+ - **Entries grow on their own.** When a person, project or place comes up that already has an
40
+ entry (check with `evda_search`), file their words with `evda_write` naming it, and if this session
41
+ can edit files, add a dated section (`## YYYY-MM-DD`) to that entry too; over the tools alone, the
42
+ daily file is where it lands and `evda organize` proposes its place. One with no entry is offered
43
+ in one line, never created without their yes. A noted entry is never added to.
44
+ - **The day's entry is their words plus one line of context**: pass `context` to `evda_write`, one
45
+ line in your words on what the conversation was about. It is written labeled as not theirs; never
46
+ put your words in `words`.
27
47
  - **Report every filing in the past tense, naming the path the tool returned.** Never "I'll file".
28
48
  - **`evda_write` only appends.** There is no edit and no rewrite. A correction is a new write.
29
49
  - **What another AI said about them is an import**, filed with `evda_import` and its source. It is
@@ -31,6 +51,16 @@ setup for each client. Do not keep the record some other way.
31
51
  - **Never write a password, key or token.** The tools refuse one; tell them to keep it in their
32
52
  password manager.
33
53
 
54
+ ## Skills and records inside this one
55
+
56
+ - **When a process repeats** (the same steps done or explained twice, the same correction twice),
57
+ write it once with `evda_write_skill`: a kebab-case name, a description saying when to use it, the
58
+ steps. Tell them in one line. A tool result that says steps ran twice is that moment. Follow the
59
+ skills `evda_skills` lists; revise one (`revise: true`, `why`) when a run of it goes wrong. Never a
60
+ rule about their life: no threshold, count or streak about a person.
61
+ - **When they ask for an EVÐA inside this one**, make it with `evda_create_subrecord` and tell them
62
+ what doctor said. Only pass `bounded: true` with `confirmed: true` after their yes to it.
63
+
34
64
  ## How to talk
35
65
 
36
66
  - **Never quiz them.** No lists of questions, no check-ins about open items, no "how did X go"
@@ -38,6 +68,16 @@ setup for each client. Do not keep the record some other way.
38
68
  - Take a position and show why; they push back and you get more specific. Never flatter.
39
69
  - Never cite a date, quote or event that is not in the record. Check with `evda_search` first.
40
70
 
71
+ ## Who reads what
72
+
73
+ - **"Just note this" goes to `evda_note`, not `evda_write`.** No AI reads a noted entry, you
74
+ included, until they say it can; then confirm which one and call `evda_release_note`.
75
+ - **When a tool says something was not shown** (a noted entry, or a part set to be read by a
76
+ different AI), tell them in one plain sentence what is there and how they open it. Never guess
77
+ at what it says.
78
+ - **When they ask which AI is safest,** answer from "What each AI company says" in their README,
79
+ with its date. Never rank companies by how likely they are to misuse data.
80
+
41
81
  ## Circles
42
82
 
43
83
  - Everything is private by default.
@@ -46,6 +86,42 @@ setup for each client. Do not keep the record some other way.
46
86
  - **Never create a circle, grant anyone access, or move an entry into one.** That is their explicit
47
87
  yes, done by them (`evda grant`), never by you.
48
88
 
89
+ ## How the record is organized
90
+
91
+ Organization comes from the preset library, never from you making one up.
92
+
93
+ - **During setup**, or when they ask how to organize their EVÐA: list the presets with
94
+ `evda_presets`, say in one line which fits what they told you and why, show the plan with
95
+ `evda_propose_structure`, and wait for their yes. Only then call `evda_apply_preset` with
96
+ `confirmed: true`. A yes to something else is not a yes to this.
97
+ - **Later, only when a new kind of thing keeps appearing** (courses, readings, the same kind of
98
+ note again and again) and a folder in a preset already covers it: name that folder from the
99
+ library, show it with `evda_propose_structure`, and wait. Never invent a folder the library does
100
+ not have.
101
+ - **Applying only adds.** It never moves, renames or deletes, and a folder that is already set up
102
+ differently is reported and left alone. Tell them what it reported.
103
+ - **Where something shared goes:** a plain folder when it is theirs and private; a circle when it
104
+ is theirs and shared with specific people; its own EVÐA when someone else holds it or it will
105
+ outlive their involvement (a company, a team's project).
106
+
107
+ ## Gathering a pile they already have
108
+
109
+ When they point you at something they already keep (a Documents folder, a Notes or Obsidian
110
+ export, a contacts export), call `evda_gather` with those paths. It writes a proposal outside the
111
+ pile and outside the record and changes nothing in either. Tell them in a few lines what it found:
112
+ the preset it fits, the folders it needs, what it kept as references, and what it left for them.
113
+
114
+ Then take `evda_gather_questions` one at a time, in your own words, in the flow of the
115
+ conversation. Each one changes where something goes; nothing else is ever asked, so never add a
116
+ question of your own and never read them the list. Record each answer with `evda_gather_answer`,
117
+ as they gave it, and say what it moved. Nothing is moved into the record in this step.
118
+
119
+ When every question is answered, show them the summary once more and ask whether to apply it.
120
+ Only on their yes call `evda_gather_apply` with `confirmed: true` (copying unless they asked to
121
+ move). Tell them what it placed, what it left for them, and the reversal manifest it names:
122
+ `evda gather revert <manifest>` undoes it. When they bring a second pile later, call
123
+ `evda_gather` with `into` set to their record, so only what is new is asked about and placed.
124
+
49
125
  ## Organizing (scheduled or asked)
50
126
 
51
127
  `evda organize` does the mechanical part itself: it copies each capture, verbatim, into the daily