create-evda 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -3
- package/bin/evda.mjs +70 -1
- package/create-evda.mjs +27 -3
- package/lib/clients.mjs +48 -0
- package/lib/connect.mjs +84 -0
- package/lib/mcp.mjs +258 -0
- package/lib/organize.mjs +224 -0
- package/lib/routines.mjs +143 -0
- package/lib/setup.mjs +66 -9
- package/lib/tools.mjs +274 -0
- package/package.json +5 -3
- package/skills/evda/SKILL.md +70 -0
- package/templates/AGENTS.md +4 -0
- package/templates/evda-digest.md +1 -0
package/lib/tools.mjs
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// The record operations behind `evda mcp`: what an AI the owner already uses may do to their EVDA.
|
|
2
|
+
//
|
|
3
|
+
// Every tool here works on plain files on this machine. Nothing is sent anywhere and no model is
|
|
4
|
+
// called. The AI that calls these tools is the owner's own (Claude Code, Codex, the Claude app);
|
|
5
|
+
// EVDA holds no key and makes no request.
|
|
6
|
+
//
|
|
7
|
+
// Three rules hold for every write, and each has a test in test/tools.test.mjs:
|
|
8
|
+
// 1. Append only. A write adds bytes to the end of a file or creates a new one. No tool takes a
|
|
9
|
+
// path to write to, and none replaces what is there.
|
|
10
|
+
// 2. Nothing secret-shaped is written. The words are run through the same check that guards a
|
|
11
|
+
// reference (lib/references.mjs secretShape) and a hit refuses the whole write.
|
|
12
|
+
// 3. Every write says where it landed, as a path relative to the record.
|
|
13
|
+
//
|
|
14
|
+
// A circle is never created here. `propose_circle` writes a proposal file for the owner to read;
|
|
15
|
+
// making the circle, and granting anyone anything, stays the owner's explicit yes through
|
|
16
|
+
// `evda grant` or their own hands.
|
|
17
|
+
|
|
18
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join, relative } from "node:path";
|
|
20
|
+
import { resolveRole, rolesForLayer, agentsFile } from "./roles.mjs";
|
|
21
|
+
import { mdFiles, fileDate } from "./record.mjs";
|
|
22
|
+
import { readCircles } from "./circles.mjs";
|
|
23
|
+
import { secretShape } from "./references.mjs";
|
|
24
|
+
import { doctor } from "./doctor.mjs";
|
|
25
|
+
import { EvdaError } from "./errors.mjs";
|
|
26
|
+
|
|
27
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
28
|
+
export const localDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
29
|
+
export const localTime = (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
30
|
+
|
|
31
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
32
|
+
const MAX_WORDS = 200_000;
|
|
33
|
+
const MAX_READ = 256_000;
|
|
34
|
+
|
|
35
|
+
function refuse(message, code = "REFUSED") {
|
|
36
|
+
return new EvdaError(`REFUSED: ${message} Nothing was written.`, { code, exit: 2 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Arguments a tool does not take are refused by name, so an agent that tries to pass a path or a
|
|
40
|
+
// replacement learns the tool cannot do that, instead of the key being silently ignored.
|
|
41
|
+
function onlyKeys(tool, args, allowed, hint = "") {
|
|
42
|
+
const extra = Object.keys(args ?? {}).filter((k) => !allowed.includes(k));
|
|
43
|
+
if (extra.length) throw refuse(`${tool} does not take ${extra.map((k) => JSON.stringify(k)).join(", ")}.${hint ? ` ${hint}` : ""}`, "UNKNOWN_ARGUMENT");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function text(value, name, tool) {
|
|
47
|
+
if (typeof value !== "string" || value.trim() === "") throw refuse(`${tool} needs "${name}", the text to file.`, "EMPTY");
|
|
48
|
+
if (value.length > MAX_WORDS) throw refuse(`${tool} "${name}" is ${value.length} characters; one write holds at most ${MAX_WORDS}.`, "TOO_LONG");
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Refuse the write when any line carries something shaped like a password, key or token. The value
|
|
53
|
+
// is never echoed back: the message names the shape and the line number only.
|
|
54
|
+
export function refuseSecrets(value, tool) {
|
|
55
|
+
const lines = value.split("\n");
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
const shape = secretShape(lines[i]);
|
|
58
|
+
if (shape) throw refuse(`${tool}: line ${i + 1} holds ${shape}. A password, key or token is never written into the record; keep it in a password manager and point at the item with a reference.`, "SECRET");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rel(evda, abs) {
|
|
63
|
+
return relative(evda.dir, abs).split("\\").join("/");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function dailyDir(evda) {
|
|
67
|
+
const r = resolveRole(evda, "record/daily");
|
|
68
|
+
if (!r.mapped) throw refuse('this record maps no "record/daily" role, so there is no daily file to append to.', "NO_ROLE");
|
|
69
|
+
return r;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Where imports go: `imports/` beside the daily role, so `record/daily` gives `record/imports`, and
|
|
73
|
+
// an adopted `journal/days` gives `journal/imports`.
|
|
74
|
+
export function importsDir(evda) {
|
|
75
|
+
const r = dailyDir(evda);
|
|
76
|
+
return { abs: join(dirname(r.abs), "imports"), rel: join(dirname(r.rel), "imports").split("\\").join("/") };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Proposals live at the record root, outside circles/ and grants/, so nothing that reads access
|
|
80
|
+
// can mistake one for a decision.
|
|
81
|
+
export const PROPOSALS = "proposals";
|
|
82
|
+
|
|
83
|
+
// 1. The operating layer, whole. The first thing an agent reads.
|
|
84
|
+
export function operatingLayer(evda, args = {}) {
|
|
85
|
+
onlyKeys("evda_operating_layer", args, []);
|
|
86
|
+
const a = agentsFile(evda);
|
|
87
|
+
if (!a) throw new EvdaError('this record maps no "agents" role, so it has no operating layer.', { code: "NO_ROLE" });
|
|
88
|
+
return { path: a.rel, text: readFileSync(a.abs, "utf8") };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. The last `days` days of the record, newest first, whole files, capped in size. A file is in the
|
|
92
|
+
// window by its date (filename, then a date or created key), the same rule `evda moment` uses.
|
|
93
|
+
export function recent(evda, args = {}, { now = new Date() } = {}) {
|
|
94
|
+
onlyKeys("evda_recent", args, ["days"]);
|
|
95
|
+
const days = args.days === undefined ? 3 : Number(args.days);
|
|
96
|
+
if (!Number.isInteger(days) || days < 1 || days > 90) throw new EvdaError("evda_recent days must be a whole number from 1 to 90.", { code: "BAD_ARGUMENT", exit: 2 });
|
|
97
|
+
const start = new Date(now);
|
|
98
|
+
start.setDate(start.getDate() - (days - 1));
|
|
99
|
+
const from = localDate(start);
|
|
100
|
+
const to = localDate(now);
|
|
101
|
+
const files = [];
|
|
102
|
+
for (const abs of recordFiles(evda)) {
|
|
103
|
+
const body = readFileSync(abs, "utf8");
|
|
104
|
+
const date = fileDate(abs, body);
|
|
105
|
+
if (!date || date < from || date > to) continue;
|
|
106
|
+
files.push({ date, path: rel(evda, abs), text: body });
|
|
107
|
+
}
|
|
108
|
+
files.sort((a, b) => (a.date === b.date ? a.path.localeCompare(b.path) : b.date.localeCompare(a.date)));
|
|
109
|
+
let used = 0;
|
|
110
|
+
const out = [];
|
|
111
|
+
let truncated = 0;
|
|
112
|
+
for (const f of files) {
|
|
113
|
+
if (used + f.text.length > MAX_READ) { truncated += 1; continue; }
|
|
114
|
+
used += f.text.length;
|
|
115
|
+
out.push(f);
|
|
116
|
+
}
|
|
117
|
+
return { from, to, files: out, omitted: truncated };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Every markdown file the record holds: each record-layer role, and the imports folder. Not the
|
|
121
|
+
// ledger and not the operating layer; those have their own tools.
|
|
122
|
+
function recordFiles(evda) {
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
const out = [];
|
|
125
|
+
const add = (dir) => {
|
|
126
|
+
if (!dir || !existsSync(dir) || !statSync(dir).isDirectory()) return;
|
|
127
|
+
for (const p of mdFiles(dir)) if (!seen.has(p)) { seen.add(p); out.push(p); }
|
|
128
|
+
};
|
|
129
|
+
for (const role of rolesForLayer("record")) {
|
|
130
|
+
const r = resolveRole(evda, role);
|
|
131
|
+
if (r.mapped && r.exists && r.isDir) add(r.abs);
|
|
132
|
+
}
|
|
133
|
+
try { add(importsDir(evda).abs); } catch { /* no daily role, so no imports folder */ }
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. Append the owner's words, verbatim, to today's daily file, under the local time. Creates the
|
|
138
|
+
// file with a date heading when it is the first write of the day. Returns the path.
|
|
139
|
+
export function write(evda, args = {}, { now = new Date() } = {}) {
|
|
140
|
+
onlyKeys("evda_write", args, ["words"], "It appends to today's daily file and nothing else: it takes no path, and it never replaces or edits what is already written.");
|
|
141
|
+
const words = text(args.words, "words", "evda_write");
|
|
142
|
+
refuseSecrets(words, "evda_write");
|
|
143
|
+
const r = dailyDir(evda);
|
|
144
|
+
mkdirSync(r.abs, { recursive: true });
|
|
145
|
+
const date = localDate(now);
|
|
146
|
+
const abs = join(r.abs, `${date}.md`);
|
|
147
|
+
const head = existsSync(abs) ? "" : `# ${date}\n`;
|
|
148
|
+
const lead = existsSync(abs) && !readFileSync(abs, "utf8").endsWith("\n") ? "\n" : "";
|
|
149
|
+
appendFileSync(abs, `${lead}${head}\n## ${localTime(now)}\n\n${words.replace(/\n+$/, "")}\n`);
|
|
150
|
+
return { path: rel(evda, abs), bytes: Buffer.byteLength(words) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 4. File what another AI said about the owner, verbatim, under imports/, labeled with its source
|
|
154
|
+
// and date. A second import from the same source on the same day is appended to that file.
|
|
155
|
+
export function importText(evda, args = {}, { now = new Date() } = {}) {
|
|
156
|
+
onlyKeys("evda_import", args, ["text", "source", "date"], "It files a new import or appends to that day's import from the same source; it never edits one.");
|
|
157
|
+
const body = text(args.text, "text", "evda_import");
|
|
158
|
+
if (typeof args.source !== "string" || args.source.trim() === "") throw refuse('evda_import needs "source": which AI or app said this (ChatGPT, Claude, Gemini...).', "NO_SOURCE");
|
|
159
|
+
const source = args.source.trim().replace(/\s+/g, " ");
|
|
160
|
+
if (source.length > 60) throw refuse('evda_import "source" is a name, at most 60 characters.', "BAD_SOURCE");
|
|
161
|
+
const date = args.date === undefined ? localDate(now) : String(args.date);
|
|
162
|
+
if (!DATE.test(date)) throw refuse('evda_import "date" must be YYYY-MM-DD.', "BAD_DATE");
|
|
163
|
+
refuseSecrets(body, "evda_import");
|
|
164
|
+
refuseSecrets(source, "evda_import");
|
|
165
|
+
const dir = importsDir(evda);
|
|
166
|
+
mkdirSync(dir.abs, { recursive: true });
|
|
167
|
+
const slug = source.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "import";
|
|
168
|
+
const abs = join(dir.abs, `${date}-${slug}.md`);
|
|
169
|
+
const block = `\n## Imported ${localDate(now)} ${localTime(now)}\n\n${body.replace(/\n+$/, "")}\n`;
|
|
170
|
+
if (existsSync(abs)) appendFileSync(abs, block);
|
|
171
|
+
else {
|
|
172
|
+
writeFileSync(abs, [
|
|
173
|
+
`# Import from ${source}, ${date}`,
|
|
174
|
+
"",
|
|
175
|
+
`source: ${source}`,
|
|
176
|
+
`date: ${date}`,
|
|
177
|
+
"",
|
|
178
|
+
`What ${source} said about the owner, filed verbatim. These are not the owner's words. Each line is a claim to check with them over time, not a fact.`,
|
|
179
|
+
"",
|
|
180
|
+
].join("\n") + block);
|
|
181
|
+
}
|
|
182
|
+
return { path: rel(evda, abs), bytes: Buffer.byteLength(body) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 5. Plain text search inside the record, on this machine. Case-insensitive substring over every
|
|
186
|
+
// record file, the imports, and the ledger. Returns each matching line with its path and number.
|
|
187
|
+
export function search(evda, args = {}) {
|
|
188
|
+
onlyKeys("evda_search", args, ["query", "limit"]);
|
|
189
|
+
if (typeof args.query !== "string" || args.query.trim() === "") throw new EvdaError('evda_search needs "query".', { code: "BAD_ARGUMENT", exit: 2 });
|
|
190
|
+
const limit = args.limit === undefined ? 50 : Number(args.limit);
|
|
191
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new EvdaError("evda_search limit must be a whole number from 1 to 500.", { code: "BAD_ARGUMENT", exit: 2 });
|
|
192
|
+
const needle = args.query.toLowerCase();
|
|
193
|
+
const files = recordFiles(evda);
|
|
194
|
+
const ledger = resolveRole(evda, "ledger");
|
|
195
|
+
if (ledger.mapped && ledger.fileExists) files.push(ledger.file);
|
|
196
|
+
const matches = [];
|
|
197
|
+
let more = 0;
|
|
198
|
+
for (const abs of files) {
|
|
199
|
+
readFileSync(abs, "utf8").split("\n").forEach((line, i) => {
|
|
200
|
+
if (!line.toLowerCase().includes(needle)) return;
|
|
201
|
+
if (matches.length >= limit) { more += 1; return; }
|
|
202
|
+
matches.push({ path: rel(evda, abs), line: i + 1, text: line.length > 400 ? `${line.slice(0, 400)}...` : line });
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return { query: args.query, searched: files.length, matches, more };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 6. Circles and their policies, as the record declares them. Read only.
|
|
209
|
+
export function circles(evda, args = {}) {
|
|
210
|
+
onlyKeys("evda_circles", args, []);
|
|
211
|
+
const all = readCircles(evda);
|
|
212
|
+
if (all === null) return { role: false, circles: [] };
|
|
213
|
+
return {
|
|
214
|
+
role: true,
|
|
215
|
+
circles: all.map((c) => ({
|
|
216
|
+
name: c.name,
|
|
217
|
+
path: c.rel,
|
|
218
|
+
public: c.manifest?.public ?? null,
|
|
219
|
+
description: c.manifest?.description ?? null,
|
|
220
|
+
maintainers: Array.isArray(c.manifest?.maintainers) ? c.manifest.maintainers.map((m) => m?.name).filter(Boolean) : [],
|
|
221
|
+
members: Object.keys(c.members),
|
|
222
|
+
policy: (c.policy?.rules ?? []).map((r) => ({ rule: r.rule, status: r.status, since: r.since })),
|
|
223
|
+
problems: c.problems.filter((p) => p.level === "fail").map((p) => p.message),
|
|
224
|
+
})),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 7. Propose a circle. Writes one new file under proposals/ and nothing else: no circle directory,
|
|
229
|
+
// no eco.yaml, no grant. The owner reads it and says yes or no.
|
|
230
|
+
export function proposeCircle(evda, args = {}, { now = new Date() } = {}) {
|
|
231
|
+
onlyKeys("evda_propose_circle", args, ["name", "who", "what", "policy", "why"], "A proposal is a file for the owner to read. Creating the circle, adding a reader or moving an entry into it waits for their yes and is not done through this tool.");
|
|
232
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
233
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) throw refuse('evda_propose_circle "name" must be letters, digits, - and _ (it would name the circle folder).', "BAD_NAME");
|
|
234
|
+
const who = text(args.who, "who", "evda_propose_circle");
|
|
235
|
+
const what = text(args.what, "what", "evda_propose_circle");
|
|
236
|
+
const why = args.why === undefined ? "" : text(args.why, "why", "evda_propose_circle");
|
|
237
|
+
const policy = args.policy === undefined ? [] : args.policy;
|
|
238
|
+
if (!Array.isArray(policy) || policy.some((p) => typeof p !== "string" || p.trim() === "")) throw refuse('evda_propose_circle "policy" is a list of rules, one sentence each, in the owner\'s words.', "BAD_POLICY");
|
|
239
|
+
for (const s of [who, what, why, ...policy]) refuseSecrets(s, "evda_propose_circle");
|
|
240
|
+
const date = localDate(now);
|
|
241
|
+
const dir = join(evda.dir, PROPOSALS);
|
|
242
|
+
mkdirSync(dir, { recursive: true });
|
|
243
|
+
let abs = join(dir, `${date}-circle-${name}.md`);
|
|
244
|
+
for (let n = 2; existsSync(abs); n++) abs = join(dir, `${date}-circle-${name}-${n}.md`);
|
|
245
|
+
writeFileSync(abs, [
|
|
246
|
+
`# Proposed circle: ${name}`,
|
|
247
|
+
"",
|
|
248
|
+
`Proposed ${date}. Not a circle yet: nothing was created and nobody was granted anything. It becomes one only when the owner says yes.`,
|
|
249
|
+
"",
|
|
250
|
+
"## Who may read it",
|
|
251
|
+
"",
|
|
252
|
+
who.trim(),
|
|
253
|
+
"",
|
|
254
|
+
"## What it covers",
|
|
255
|
+
"",
|
|
256
|
+
what.trim(),
|
|
257
|
+
"",
|
|
258
|
+
"## Policy",
|
|
259
|
+
"",
|
|
260
|
+
...(policy.length ? policy.map((p) => `- ${p.trim()}`) : ["(none proposed yet)"]),
|
|
261
|
+
"",
|
|
262
|
+
...(why ? ["## Why", "", why.trim(), ""] : []),
|
|
263
|
+
].join("\n"));
|
|
264
|
+
const circlesRole = resolveRole(evda, "circles");
|
|
265
|
+
return { path: rel(evda, abs), circleCreated: false, circlesRole: circlesRole.mapped ? circlesRole.rel : null };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 8. evda doctor, captured. Exit 0 is well formed.
|
|
269
|
+
export function doctorReport(evda, args = {}) {
|
|
270
|
+
onlyKeys("evda_doctor", args, []);
|
|
271
|
+
const lines = [];
|
|
272
|
+
const r = doctor(evda.dir, { out: (l) => lines.push(l), via: "evda mcp" });
|
|
273
|
+
return { exit: r.exit, text: lines.join("\n") };
|
|
274
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-evda",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Make your own EVÐA: a running journal of your life with Iris, as plain files on your machine, for any AI. No runtime dependencies.",
|
|
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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-evda": "create-evda.mjs",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"bin",
|
|
15
15
|
"lib",
|
|
16
16
|
"templates",
|
|
17
|
+
"skills",
|
|
17
18
|
"migrations",
|
|
18
19
|
"create-evda.mjs",
|
|
19
20
|
"README.md",
|
|
@@ -23,7 +24,8 @@
|
|
|
23
24
|
"evda",
|
|
24
25
|
"personal-record",
|
|
25
26
|
"agents",
|
|
26
|
-
"markdown"
|
|
27
|
+
"markdown",
|
|
28
|
+
"mcp"
|
|
27
29
|
],
|
|
28
30
|
"author": "Mike Veson",
|
|
29
31
|
"license": "MIT",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: evda
|
|
3
|
+
description: Use when the person talks to their EVÐA from Claude Code, Codex or the Claude app through the evda MCP server, or says "evda", "Iris", "my record", "journal this", "log this", or freewrites about their day, what is on their mind, a person, or something that felt off. Also use for the scheduled organize and look back routines. Acts as Iris through the evda_* tools, files their words verbatim, and never quizzes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# EVÐA
|
|
7
|
+
|
|
8
|
+
The person's EVÐA is their record: plain files on their machine, reached through the `evda` MCP
|
|
9
|
+
server. You are the agent it describes (Iris unless they renamed you). EVÐA holds no API key and
|
|
10
|
+
calls no model; you are their own AI, and everything you keep goes through the tools below.
|
|
11
|
+
|
|
12
|
+
## Every session
|
|
13
|
+
|
|
14
|
+
1. **Call `evda_operating_layer` first and follow it for the whole session.** It is who you are,
|
|
15
|
+
what this EVÐA is for in their words, how the record is kept, and what they have taught you.
|
|
16
|
+
Where it and this skill disagree, it wins.
|
|
17
|
+
2. Call `evda_recent` for the last few days, and read what they point you at with `evda_search`.
|
|
18
|
+
3. Say in one line what you read, then answer them.
|
|
19
|
+
|
|
20
|
+
If the tools are not there, say so plainly and point them at `evda connect`, which prints the
|
|
21
|
+
setup for each client. Do not keep the record some other way.
|
|
22
|
+
|
|
23
|
+
## Keeping the record
|
|
24
|
+
|
|
25
|
+
- **File their words in the same turn they say them**, with `evda_write`: verbatim, filler, hedges
|
|
26
|
+
and typos intact. Never a summary, never your words.
|
|
27
|
+
- **Report every filing in the past tense, naming the path the tool returned.** Never "I'll file".
|
|
28
|
+
- **`evda_write` only appends.** There is no edit and no rewrite. A correction is a new write.
|
|
29
|
+
- **What another AI said about them is an import**, filed with `evda_import` and its source. It is
|
|
30
|
+
not their words: never quote it back as something they said. Each line is a claim to check.
|
|
31
|
+
- **Never write a password, key or token.** The tools refuse one; tell them to keep it in their
|
|
32
|
+
password manager.
|
|
33
|
+
|
|
34
|
+
## How to talk
|
|
35
|
+
|
|
36
|
+
- **Never quiz them.** No lists of questions, no check-ins about open items, no "how did X go"
|
|
37
|
+
unless they bring it up. One real question when it helps, at most.
|
|
38
|
+
- Take a position and show why; they push back and you get more specific. Never flatter.
|
|
39
|
+
- Never cite a date, quote or event that is not in the record. Check with `evda_search` first.
|
|
40
|
+
|
|
41
|
+
## Circles
|
|
42
|
+
|
|
43
|
+
- Everything is private by default.
|
|
44
|
+
- **Only when they ask to share something**, propose a circle with `evda_propose_circle`: who may
|
|
45
|
+
read it, what it covers, a short policy in their words. Tell them the proposal's path and wait.
|
|
46
|
+
- **Never create a circle, grant anyone access, or move an entry into one.** That is their explicit
|
|
47
|
+
yes, done by them (`evda grant`), never by you.
|
|
48
|
+
|
|
49
|
+
## Organizing (scheduled or asked)
|
|
50
|
+
|
|
51
|
+
`evda organize` does the mechanical part itself: it copies each capture, verbatim, into the daily
|
|
52
|
+
file of its date and writes a report under `reports/organize/`. With `--with-ai` it then asks you,
|
|
53
|
+
through `claude -p`, for the part that needs judgment, and appends your answer to that report.
|
|
54
|
+
|
|
55
|
+
When you are asked for that judgment:
|
|
56
|
+
|
|
57
|
+
1. Read the report you are given, then use the read tools (`evda_recent`, `evda_search`,
|
|
58
|
+
`evda_circles`) to check each item against the record.
|
|
59
|
+
2. For each note that needs a place, answer in one line: the note's path, where it belongs (a
|
|
60
|
+
person's file, a project, the ledger only if they already agreed to it), and the words from the
|
|
61
|
+
note that say so, quoted.
|
|
62
|
+
3. You write nothing yourself here. Move nothing, rename nothing, delete nothing, and never touch
|
|
63
|
+
`circles/` or `grants/`. The owner reads the report and decides.
|
|
64
|
+
4. If nothing needs a place, say that in one line.
|
|
65
|
+
|
|
66
|
+
## Look back (weekly)
|
|
67
|
+
|
|
68
|
+
Read the week with `evda_recent` (7 days). Write back, in a few lines, what recurred in their own
|
|
69
|
+
words and what changed, quoting the file you took each from. No score, no streak, no grades, and
|
|
70
|
+
no rule with a number in it. If nothing stands out, say that.
|
package/templates/AGENTS.md
CHANGED
|
@@ -221,6 +221,10 @@ help with that too. There is almost always a way to live well in the situation t
|
|
|
221
221
|
- **Their words stay their words.** Quote verbatim into `record/` for the day: every word, including filler like "ok so", hedges and typos intact.
|
|
222
222
|
Never present your own summary as something they said, and never treat something you wrote earlier
|
|
223
223
|
as evidence of what they think.
|
|
224
|
+
- **An import is what another AI said about them, not their own words.** When they paste in what
|
|
225
|
+
an AI they use elsewhere already knows or has guessed about them, file it verbatim under
|
|
226
|
+
`record/imports/YYYY-MM-DD-<source>.md`, source and date at the top. Never quote a line from it
|
|
227
|
+
back as something they said; treat each item as a claim to check with them over time, not a fact.
|
|
224
228
|
- **Report filings in the past tense, naming the path.** Never "I'll file that"; do it, then say where.
|
|
225
229
|
- **Keep what already exists current.** When something new surfaces about a person, project or place
|
|
226
230
|
that already has an entry, add a dated section to it in the same session. Never overwrite. Record
|
package/templates/evda-digest.md
CHANGED
|
@@ -49,6 +49,7 @@ Instruction-only digest of the generated block of AGENTS.md. Read AGENTS.md in f
|
|
|
49
49
|
## Keeping the record
|
|
50
50
|
- Act first, in the same turn
|
|
51
51
|
- Their words stay their words
|
|
52
|
+
- An import is what another AI said about them, not their own words
|
|
52
53
|
- Report filings in the past tense, naming the path
|
|
53
54
|
- Keep what already exists current
|
|
54
55
|
- Never cite a date, quote or event that is not in the record
|