create-evda 0.1.1 → 0.1.3

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 CHANGED
@@ -1,41 +1,74 @@
1
- # @evda/person-record
1
+ # create-evda
2
2
 
3
- The person layer over the substrate. An EVÐA is a directory that knows what it is: a person's record in their own words, a ledger of claims about them and what happened to each claim, and the operating instructions beside them, so that any agent on any model can open it and act correctly with no outside context.
3
+ EVÐA is a really organized journal that helps you live your life the more you use it. Use it on every part of your life: what stresses you out, what you are uncertain of, the things you do every day that feel slightly off. Freewrite with Iris, and you get better at seeing your thoughts, understanding why you do what you do, why certain people matter to you and why you react the way you do.
4
4
 
5
- This package scaffolds one and ships the optional tooling. The files are the truth. Nothing here is required to read or write a record; `evda doctor` says whether one is well-formed and `evda reindex` rebuilds the derived directory, and both can be thrown away. It has no runtime dependencies (`test/deps.test.mjs` asserts it) and never makes a network call.
5
+ Iris cannot give you the right answer for your life, but it is really good at suggestions. Take them, push back on what you disagree with, get specific, and get a new response back. Iris keeps your words in your EVÐA as you talk. EVÐA is not a tool to do what you already do a little better: it helps you learn what is worth doing, do that better, revise, and continue. We assume far more about ourselves, our situations and other people than we know, and you learn to be specific about those assumptions.
6
6
 
7
- Design contract: Spec 04 (the hypothesis ledger), Spec 05 (the data layer) and Spec 07 (holders, circles, references and views) in the EVÐA project. A short copy of what this package settles is in `docs/canon/person-record.md`.
7
+ It is not easy to write down thoughts you have not admitted to yourself, never mind to a friend or a therapist. EVÐA is designed for exactly that, and it is open source to lower that barrier as far as it goes. Your record is plain files on your own machine and it is never accessible to anyone at EVÐA: the company never receives it. The AI you choose does receive what Iris reads, which is why setup has you turn model training off first. Capable open source models you run on your own hardware are the next step, so you know exactly where your words go.
8
8
 
9
- ## Where it sits
9
+ EVÐA prescribes no values and no rules. The only requirement is that you want to, and you are open to it. It is built on a neo-Aristotelian philosophy; ask your agent about it, or have any agent verify it by reading this code.
10
10
 
11
- Block tier, domain neutral. It imports nothing from the workspace at runtime. What it shares with the substrate is a file format: a circle is an eco, declared by the same `eco.yaml` that `@evda/common` parses and `@evda/vault-core` stores, and `test/eco-parity.test.mjs` runs every manifest this package writes through the substrate's own parser. Grants, entity addressing, changes and sync stay the substrate's; this package reads and writes plain files and leaves those to it.
11
+ ```bash
12
+ npx create-evda my-evda
13
+ ```
12
14
 
13
- ## Scaffold
15
+ ## Setup
16
+
17
+ On a terminal, `create-evda` asks one question at a time: what to call your agent (Iris unless you rename it), who you are, what you want this for, how it should talk to you, and which AI you will use. It shows where that AI's training setting is, links the provider's own page, and does not finish until you type yes. The answers go into "What this EVÐA is" at the top of `AGENTS.md`, in your words. It suggests dictation: Apple's built in dictation, or Wispr Flow with data collection turned off. Last, it offers to bring in what an AI you already use knows about you; see below.
18
+
19
+ Unattended, nothing is asked:
14
20
 
15
21
  ```bash
16
- node create-evda.mjs my-evda --owner "Dana"
22
+ npx create-evda my-evda --yes --agent-name Iris --owner "Dana" --purpose "why work stresses me out" \
23
+ --voice "direct" --platform claude --training-off
17
24
  ```
18
25
 
26
+ `--platform` takes `claude`, `chatgpt` (ChatGPT and Codex), `gemini`, `cursor` or `other`. Without `--training-off` the AI line stays empty and the agent asks about training in the first session. A run with no terminal on stdin is treated as `--yes`.
27
+
28
+ ## Any AI
29
+
30
+ `AGENTS.md` is the one source. Codex, Cursor, GitHub Copilot, Windsurf, Zed, opencode and the other tools on agents.md read it directly. For the two that do not, the scaffold writes a one line loader: `CLAUDE.md` for Claude Code and `GEMINI.md` for Gemini CLI, each `@AGENTS.md`. Aider takes `aider --read AGENTS.md`. The Claude desktop app reads it too, once its Filesystem extension is installed and pointed at this folder (Settings, then Extensions, then Filesystem). ChatGPT and Claude in the browser or app, used through a Project, cannot read or write this folder: a Project's instructions field holds text for that conversation only, and pasting `AGENTS.md` into it does not run an EVDA, because nothing you write there reaches `record/`. Bring what a chat app like that already knows about you in once, as an import, instead: see "Bringing in what an AI already knows" below.
31
+
32
+ ## Bringing in what an AI already knows
33
+
34
+ Setup ends by asking whether to bring in what an AI you already talk to knows about you. Say yes and it prints a prompt: paste that into ChatGPT, Claude, or whatever you use, and it writes out what it remembers or has guessed about you, in plain text. Bring the answer to your first session and your agent files it verbatim under `record/imports/YYYY-MM-DD-<source>.md`, with the source and date at the top. It is filed as what that AI said, never as your own words, and every line in it is a claim to check with you over time, not a fact.
35
+
36
+ ## Circles and other people
37
+
38
+ Everything is private until you share it. A circle is a part of the record you choose to share, plus who may read it. Audience and content are set separately: a public circle has the widest audience and the least content. Ask the agent to share something and it proposes a circle with a plain policy and waits for your yes; it never creates one, adds a reader, or moves an entry on its own. Another person reads a circle through a grant naming their own record (`evda grant family --to sam.atlas --kind person`), and `evda revoke` takes it back. A part can be more private than its circle, never less.
39
+
40
+ ## The package
41
+
42
+ `@evda/person-record` in this repository, published as `create-evda`. The person layer over the substrate. An EVÐA is a directory that knows what it is: a person's record in their own words, a ledger of reads about them held loosely, and the operating instructions beside them, so that any agent on any model can open it and act correctly with no outside context. The files are the truth. Nothing here is required to read or write a record; `evda doctor` says whether one is well formed and `evda reindex` rebuilds the derived directory, and both can be thrown away. It has no runtime dependencies (`test/deps.test.mjs` asserts it) and never makes a network call.
43
+
44
+ Design contract: Spec 04 (the hypothesis ledger), Spec 05 (the data layer), Spec 06 (the guarantee) and Spec 07 (holders, circles, references and views) in the EVÐA project. A short copy of what this package settles is in `docs/canon/person-record.md`.
45
+
46
+ ## Where it sits
47
+
48
+ Block tier, domain neutral. It imports nothing from the workspace at runtime. What it shares with the substrate is a file format: a circle is an eco, declared by the same `eco.yaml` that `@evda/common` parses and `@evda/vault-core` stores, and `test/eco-parity.test.mjs` runs every manifest this package writes through the substrate's own parser. Grants, entity addressing, changes and sync stay the substrate's; this package reads and writes plain files and leaves those to it.
49
+
50
+ ## Scaffold
51
+
19
52
  Writes:
20
53
 
21
54
  my-evda/
22
55
  evda.json schema version, layers, and the MAPPING of roles to paths
23
- README.md what this is and how it works, for the owner, not the agent
56
+ README.md what this is, how to turn training off, how circles work: for the owner
24
57
  AGENTS.md the operating layer. One installer-owned fence; the rest is the owner's
25
- CLAUDE.md one line: @AGENTS.md (Claude Code does not read AGENTS.md on its own)
58
+ CLAUDE.md, GEMINI.md one line each, @AGENTS.md, for Claude Code and Gemini CLI
26
59
  record/daily/ YYYY-MM-DD.md, verbatim. Day one holds one file: today's, recording
27
60
  that the record was created. Everything after it is the owner's.
28
61
  record/captures/ each record folder carries folder.evda.json: what its entries are
29
62
  record/people/ (record, capture, person), which connectors feed its references,
30
63
  and which views render it
31
- ledger/hypotheses.md the entry shape and the six rules are at the top of the file
64
+ ledger/hypotheses.md the entry shape and the rules are at the top of the file
32
65
  circles/, grants/ with --layers record,ledger,circles: each circle's eco.yaml, and one
33
66
  grant file per reader and circle
34
67
  .evda/version the schema version actually on disk
35
68
  .evda/due.jsonl derived from the ledger's Due lines
36
- .evda/digest.md derived from the fenced block of AGENTS.md, under 2048 bytes
69
+ .evda/digest.md derived from the fenced block of AGENTS.md, under 4096 bytes
37
70
 
38
- Then open the directory in Claude Code, or any agent that reads AGENTS.md, and say something true about today. It goes into `record/daily/` in your words. Nothing is proposed about you until the record holds something to be wrong about.
71
+ The training instructions in the scaffolded README and in setup come from one table, `lib/setup.mjs`, with a link to each provider's page.
39
72
 
40
73
  The tree above is what the scaffolder creates and not a requirement. `evda.json` carries a `mapping` from roles to paths, and every tool reads roles. An existing folder of notes becomes an EVÐA by adding `evda.json` with a mapping that names where things already are, and nothing moves.
41
74
 
@@ -196,7 +229,7 @@ Nothing in this package belongs to one person's record. Two things that do are r
196
229
  ## Checks
197
230
 
198
231
  ```bash
199
- pnpm --filter @evda/person-record test # node --test, 101 tests
232
+ pnpm --filter @evda/person-record test # node --test
200
233
  pnpm --filter @evda/person-record lint # no em or en dashes; generated templates match the spec when reachable
201
234
  pnpm --filter @evda/person-record vectors:both # both readers over every vector; SKIPPED for the one not reachable
202
235
  ```
@@ -205,4 +238,4 @@ The package tests need `@evda/common` built first (`pnpm --filter @evda/common..
205
238
 
206
239
  ## Publishing
207
240
 
208
- Not published. The bins are `create-evda` and `evda`; a public package under the `create-evda` name so that `npx create-evda my-evda` works is the owner's decision and its own ticket.
241
+ Published to npm as `create-evda`, with the bins `create-evda` and `evda`. This directory is `@evda/person-record` in the workspace; the npm copy is staged from it with the same runtime files, the LICENSE, and no devDependencies.
package/bin/evda.mjs CHANGED
@@ -51,7 +51,9 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
51
51
  evda status [dir] one screen summary
52
52
  evda where which EVDA the locator resolves to, and by which rule
53
53
  evda use <dir> point ~/.evda/current at a record
54
- evda init <dir> [--name N] [--owner O] [--layers record,ledger]
54
+ evda init <dir> [--yes] [--agent-name N] [--owner O] [--purpose P] [--voice V]
55
+ [--platform claude|chatgpt|gemini|cursor|other] [--training-off] [--layers record,ledger]
56
+ the same as create-evda: asks on a terminal, runs from flags otherwise
55
57
 
56
58
  Locator order: directory argument, $EVDA_HOME, the working directory's ancestors, ~/.evda/current.
57
59
  Exit codes: 0 ok, 1 the record has a problem and it was named, 2 refused (nothing written).`;
package/create-evda.mjs CHANGED
@@ -1,14 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  // create-evda: scaffold an EVDA, a directory that knows what it is.
3
3
  //
4
- // node create-evda.mjs <dir> [--name NAME] [--owner NAME] [--layers record,ledger[,circles]] [--force]
4
+ // create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT]
5
+ // [--platform claude|chatgpt|gemini|cursor|other] [--training-off] [--yes]
6
+ // [--name NAME] [--layers record,ledger[,circles]] [--force]
5
7
  //
6
- // Writes the manifest, the operating layer, the corpus, the ledger with its own entry shape at the
7
- // top, and .evda/. Then runs `evda doctor` on what it wrote and prints the one instruction that
8
- // matters. It creates no example notes: an empty record that fills with a real life is the
9
- // product, and one seeded with fake content teaches the owner to ignore it. The one file it writes
10
- // under record/ is today's daily file holding a true event, the record's own creation, so that the
11
- // first session has something dated to read and the owner sees where their words go.
8
+ // On a terminal, any setup answer not given as a flag is asked, one question at a time, and setup
9
+ // does not finish until the owner confirms model training is off for the AI they chose. With
10
+ // --yes, or with no terminal, nothing is asked: the flags are written as given, and the AI line
11
+ // stays empty unless --training-off was passed, so the agent asks in the first session.
12
+ //
13
+ // Writes the manifest, the operating layer with the owner's answers in its owner section, the
14
+ // loaders for the tools that do not read AGENTS.md on their own, the corpus, the ledger, and
15
+ // .evda/. Then runs `evda doctor` on what it wrote. It creates no example notes: an empty record
16
+ // that fills with a real life is the product. The one file it writes under record/ is today's
17
+ // daily file holding a true event, the record's own creation, so the first session has something
18
+ // dated to read and the owner sees where their words go.
12
19
 
13
20
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
14
21
  import { join, resolve, dirname } from "node:path";
@@ -21,6 +28,7 @@ import { doctor } from "./lib/doctor.mjs";
21
28
  import { readPointer, setPointer, pointerPath } from "./lib/locate.mjs";
22
29
  import { FOLDER_MANIFEST } from "./lib/folders.mjs";
23
30
  import { EvdaError } from "./lib/errors.mjs";
31
+ import { PLATFORMS, PLATFORM_KEYS, LOADERS, DESKTOP_FILESYSTEM, DICTATION, DEFAULT_AGENT_NAME, platformKey, fillOwnerSection, aiLine, interview, trainingSection } from "./lib/setup.mjs";
24
32
 
25
33
  const HERE = dirname(fileURLToPath(import.meta.url));
26
34
  const TEMPLATES = join(HERE, "templates");
@@ -46,24 +54,48 @@ const FOLDER_TYPES = {
46
54
  "record/people": "person",
47
55
  };
48
56
 
49
- function parseArgs(argv) {
50
- const opts = { layers: ["record", "ledger"], force: false, name: null, owner: "", kind: "person", id: null, pointer: true, use: false };
57
+ const USAGE = `usage: create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT] [--platform ${PLATFORM_KEYS.join("|")}] [--training-off] [--yes] [--name NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--use] [--no-pointer]`;
58
+
59
+ export function parseArgs(argv) {
60
+ const opts = {
61
+ layers: ["record", "ledger"], force: false, name: null, owner: "", kind: "person", id: null, pointer: true, use: false,
62
+ yes: false, trainingOff: false, agentName: null, purpose: "", voice: "", platform: null, platformName: null,
63
+ };
64
+ const args = argv.flatMap((a) => {
65
+ const eq = /^(--[a-z-]+)=([\s\S]*)$/.exec(a);
66
+ return eq ? [eq[1], eq[2]] : [a];
67
+ });
51
68
  const positional = [];
52
- for (let i = 0; i < argv.length; i++) {
53
- const a = argv[i];
69
+ const value = (i, flag) => {
70
+ if (i >= args.length) throw new EvdaError(`${flag} needs a value`, { exit: 2 });
71
+ return args[i];
72
+ };
73
+ for (let i = 0; i < args.length; i++) {
74
+ const a = args[i];
54
75
  if (a === "--force") opts.force = true;
55
76
  else if (a === "--no-pointer") opts.pointer = false;
56
77
  else if (a === "--use") opts.use = true;
57
- else if (a === "--name") opts.name = argv[++i];
58
- else if (a === "--owner") opts.owner = argv[++i] ?? "";
59
- else if (a === "--kind") opts.kind = argv[++i];
60
- else if (a === "--id") opts.id = argv[++i];
61
- else if (a === "--layers") opts.layers = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
78
+ else if (a === "--yes" || a === "-y") opts.yes = true;
79
+ else if (a === "--training-off") opts.trainingOff = true;
80
+ else if (a === "--name") opts.name = value(++i, a);
81
+ else if (a === "--owner") opts.owner = value(++i, a);
82
+ else if (a === "--agent-name") opts.agentName = value(++i, a);
83
+ else if (a === "--purpose") opts.purpose = value(++i, a);
84
+ else if (a === "--voice") opts.voice = value(++i, a);
85
+ else if (a === "--platform") {
86
+ const raw = value(++i, a);
87
+ opts.platform = platformKey(raw);
88
+ if (!opts.platform) throw new EvdaError(`--platform needs one of ${PLATFORM_KEYS.join(", ")}, or the name of your AI`, { exit: 2 });
89
+ if (opts.platform === "other" && raw.trim().toLowerCase() !== "other") opts.platformName = raw.trim();
90
+ }
91
+ else if (a === "--kind") opts.kind = value(++i, a);
92
+ else if (a === "--id") opts.id = value(++i, a);
93
+ else if (a === "--layers") opts.layers = value(++i, a).split(",").map((s) => s.trim()).filter(Boolean);
62
94
  else if (a === "--quiet") opts.quiet = true;
63
95
  else if (a.startsWith("--")) throw new EvdaError(`unknown option ${a}`, { exit: 2 });
64
96
  else positional.push(a);
65
97
  }
66
- if (positional.length !== 1) throw new EvdaError("usage: create-evda <dir> [--name NAME] [--owner NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--use] [--no-pointer]", { exit: 2 });
98
+ if (positional.length !== 1) throw new EvdaError(USAGE, { exit: 2 });
67
99
  opts.dir = resolve(positional[0]);
68
100
  if (!opts.name) opts.name = opts.dir.split("/").pop();
69
101
  for (const l of opts.layers) if (!KNOWN_LAYERS.includes(l)) throw new EvdaError(`unknown layer "${l}". Known: ${KNOWN_LAYERS.join(", ")}`, { exit: 2 });
@@ -77,11 +109,15 @@ function render(name, vars) {
77
109
  return text;
78
110
  }
79
111
 
80
- export function scaffold(opts) {
81
- const { dir } = opts;
82
- if (existsSync(dir) && readdirSync(dir).length > 0 && !opts.force) {
112
+ function refuseNonEmpty(dir, force) {
113
+ if (existsSync(dir) && readdirSync(dir).length > 0 && !force) {
83
114
  throw new EvdaError(`REFUSED: ${dir} exists and is not empty. Nothing was written. Pass --force to scaffold into it anyway (existing files are never overwritten).`, { exit: 2 });
84
115
  }
116
+ }
117
+
118
+ export function scaffold(opts) {
119
+ const { dir } = opts;
120
+ refuseNonEmpty(dir, opts.force);
85
121
  const today = localDate();
86
122
  const mapping = { agents: CANONICAL.agents };
87
123
  for (const layer of opts.layers) for (const role of rolesForLayer(layer)) mapping[role] = CANONICAL[role];
@@ -95,11 +131,20 @@ export function scaffold(opts) {
95
131
  const id = opts.id ?? `${slug(opts.name)}.atlas`;
96
132
  const problems = holderProblems({ owner: { kind }, id });
97
133
  if (problems.length) throw new EvdaError(`REFUSED: ${problems.join(" ")}`, { exit: 2 });
98
- const manifest = { evda: SCHEMA_VERSION, id, name: opts.name, owner: { name: opts.owner, kind }, created: today, layers: opts.layers, mapping };
134
+ const manifest = { evda: SCHEMA_VERSION, id, name: opts.name, owner: { name: opts.owner ?? "", kind }, created: today, layers: opts.layers, mapping };
99
135
  writeIfAbsent(join(dir, "evda.json"), JSON.stringify(manifest, null, 2) + "\n");
100
- writeIfAbsent(join(dir, "README.md"), render("README.md", {}));
101
- writeIfAbsent(join(dir, mapping.agents), render("AGENTS.md", {}));
102
- writeIfAbsent(join(dir, "CLAUDE.md"), render("CLAUDE.md", { agents: mapping.agents }));
136
+ writeIfAbsent(join(dir, "README.md"), render("README.md", { training: trainingSection() }));
137
+ // The owner section of AGENTS.md holds the setup answers in the owner's words. The AI line is
138
+ // written only when training off was confirmed; empty, it is what makes the agent ask.
139
+ const answers = {
140
+ agentName: opts.agentName,
141
+ owner: opts.owner,
142
+ purpose: opts.purpose,
143
+ voice: opts.voice,
144
+ ai: aiLine({ platform: opts.platform, platformName: opts.platformName, trainingOff: opts.trainingOff, date: today }),
145
+ };
146
+ writeIfAbsent(join(dir, mapping.agents), fillOwnerSection(render("AGENTS.md", {}), answers));
147
+ for (const loader of Object.keys(LOADERS)) writeIfAbsent(join(dir, loader), render(loader, { agents: mapping.agents }));
103
148
  if (mapping.ledger) writeIfAbsent(join(dir, mapping.ledger, "hypotheses.md"), render("hypotheses.md", { date: today, name: opts.name }));
104
149
  if (mapping["record/daily"]) writeIfAbsent(join(dir, mapping["record/daily"], `${today}.md`), firstDay(today));
105
150
  if (mapping.circles) writeIfAbsent(join(dir, mapping.circles, "README.md"), render("circles-README.md", {}));
@@ -154,10 +199,37 @@ function writeIfAbsent(path, text) {
154
199
  writeFileSync(path, text);
155
200
  }
156
201
 
157
- export async function main(argv) {
158
- const opts = parseArgs(argv);
202
+ // The terminal the setup questions go to.
203
+ async function terminalIO() {
204
+ const { createInterface } = await import("node:readline/promises");
205
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
206
+ return { interactive: true, ask: (q) => rl.question(q), out: (l) => console.log(l), close: () => rl.close() };
207
+ }
208
+
209
+ export function needsAsking(opts) {
210
+ return opts.agentName == null || !opts.owner || !opts.purpose || !opts.voice || !opts.platform || !opts.trainingOff;
211
+ }
212
+
213
+ // `io` is for tests: { interactive, ask(question), out(line) }. Without it, setup is interactive
214
+ // only when both stdin and stdout are a terminal. A piped or redirected run is a script, and a
215
+ // script is never asked anything.
216
+ export async function main(argv, io = null) {
217
+ let opts = parseArgs(argv);
218
+ const out = io?.out ?? console.log;
219
+ const log = opts.quiet ? () => {} : out;
220
+ const tty = io ? Boolean(io.interactive) : Boolean(process.stdin.isTTY && process.stdout.isTTY);
221
+ const asked = tty && !opts.yes && needsAsking(opts);
222
+ if (asked) {
223
+ refuseNonEmpty(opts.dir, opts.force);
224
+ const term = io ?? (await terminalIO());
225
+ try {
226
+ opts = await interview(opts, term);
227
+ } finally {
228
+ term.close?.();
229
+ }
230
+ log("");
231
+ }
159
232
  const { dir, pointer } = scaffold(opts);
160
- const log = opts.quiet ? () => {} : console.log;
161
233
  log(`created an EVDA at ${dir}`);
162
234
  log(`locator ${pointer.path}: ${pointer.action}\n`);
163
235
  const report = doctor(dir, { out: log, via: "create-evda" });
@@ -165,10 +237,21 @@ export async function main(argv) {
165
237
  console.error("\ncreate-evda wrote the layout but doctor does not pass on it. That is a bug in create-evda; the output above says where.");
166
238
  return report.exit;
167
239
  }
168
- log(`\nOpen this directory in Claude Code, or any agent that reads AGENTS.md, and it will know what to do.`);
169
- log(`It works from your words, takes a position instead of a menu, and parks what cannot be settled yet; it never scores you. Every correction becomes a rule, in your words, in a file you can open and edit. Pushing back is how you use it: it proposes a claim specific enough to be wrong, you say what is wrong, and the next one is sharper. README.md says this again at your own pace.`);
170
- log(`First move: say something true about today. It goes into record/daily/${localDate()}.md in your words, and nothing is proposed about you until there is something there to be wrong about.`);
171
- log(`The files are the record. \`evda doctor\` checks them and \`evda reindex\` rebuilds .evda/; neither is required.`);
240
+ const agent = opts.agentName || DEFAULT_AGENT_NAME;
241
+ const chosen = opts.platform ? [opts.platform] : PLATFORM_KEYS;
242
+ log("");
243
+ log(`Open it with ${agent}:`);
244
+ for (const k of chosen) for (const line of PLATFORMS[k].open) log(` ${line}`);
245
+ if (opts.platform && opts.platform !== "other") log(` ${PLATFORMS.other.open[0]}`);
246
+ for (const line of DESKTOP_FILESYSTEM) log(` ${line}`);
247
+ if (!opts.trainingOff) {
248
+ log("");
249
+ log(`Model training is not confirmed off yet, so ${agent} asks about it first. Where to turn it off:`);
250
+ for (const k of chosen) log(` ${PLATFORMS[k].label}: ${PLATFORMS[k].training[0]}${PLATFORMS[k].url ? ` ${PLATFORMS[k].url}` : ""}`);
251
+ }
252
+ log("");
253
+ log(`Then freewrite: what happened today, what is on your mind, what felt slightly off. Your words go into record/ as you talk. README.md says the rest.`);
254
+ if (!asked) log(DICTATION);
172
255
  return 0;
173
256
  }
174
257
 
package/lib/agents.mjs CHANGED
@@ -16,7 +16,7 @@ import { EvdaError } from "./errors.mjs";
16
16
 
17
17
  export const FENCE_OPEN = "<!-- evda:generated -->";
18
18
  export const FENCE_CLOSE = "<!-- /evda:generated -->";
19
- export const DIGEST_BUDGET = 2048;
19
+ export const DIGEST_BUDGET = 4096;
20
20
  export const TEMPLATE_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "templates", "AGENTS.md");
21
21
 
22
22
  // {before, generated, after} where generated INCLUDES both fence lines, or null if no fence.
package/lib/doctor.mjs CHANGED
@@ -52,6 +52,7 @@ import { splitFence, digestFrom, digestBytes, DIGEST_BUDGET } from "./agents.mjs
52
52
  import { readCircles, coverage, LEGACY_MANIFEST } from "./circles.mjs";
53
53
  import { folderManifests, folderFor, viewNamedIn, FOLDER_MANIFEST } from "./folders.mjs";
54
54
  import { referencesIn } from "./references.mjs";
55
+ import { KIND_FOR_TYPE, PERSON_KINDS, entryFromFile, readEntry } from "./kinds.mjs";
55
56
  import { EvdaError } from "./errors.mjs";
56
57
 
57
58
  // Same rule as record.mjs fileDate: a daily file is dated by a leading YYYY-MM-DD in its name.
@@ -156,7 +157,6 @@ export function doctor(dir, { out = console.log, via = "argument" } = {}) {
156
157
  if (agents.trim() === "") fail(rel, "is empty.");
157
158
  else {
158
159
  if (!/ledger/i.test(agents)) warn(rel, "never mentions the ledger. An operating layer that does not name the loop cannot operate it.");
159
- if (!/hypothes/i.test(agents)) warn(rel, "never mentions hypotheses.");
160
160
  try {
161
161
  fence = splitFence(agents);
162
162
  } catch (e) {
@@ -357,6 +357,42 @@ export function doctor(dir, { out = console.log, via = "argument" } = {}) {
357
357
  }
358
358
  if (views === 0) skip("views", "no folder manifest declares a view");
359
359
  else ok(views, "views", `across ${folders.filter((f) => f.views.length).length} folder(s)`);
360
+
361
+ // 12c. typed entries (Spec 07, section 2): a folder whose manifest declares one of the six
362
+ // person-layer types holds entries of that kind. Each entry file (`.json`, `.yaml`, or markdown
363
+ // whose frontmatter says `kind:`) is read through lib/kinds.mjs: the SCHEMA's shape, then the
364
+ // kind's rules. A file of another kind, or one that does not read, fails with what is wrong.
365
+ // Markdown with no `kind:` is left alone; it is not claiming to be an entry.
366
+ const typed = folders.filter((f) => f.type && KIND_FOR_TYPE[f.type]);
367
+ if (typed.length === 0) {
368
+ skip("typed entries", `no folder manifest declares a person-layer type (${Object.keys(KIND_FOR_TYPE).join(", ")})`);
369
+ } else {
370
+ const owner = holder.id ?? holder.name;
371
+ const counts = {};
372
+ let entries = 0;
373
+ for (const f of typed) {
374
+ const want = KIND_FOR_TYPE[f.type];
375
+ const where = `folder ${relative(dir, f.dir) || "."}`;
376
+ const viewPaths = new Set(f.views.map((v) => resolve(dir, v.path)));
377
+ for (const p of filesUnder(f.dir)) {
378
+ if (folderFor(p, folders) !== f || viewPaths.has(p)) continue;
379
+ const name = p.slice(p.lastIndexOf(sep) + 1);
380
+ if (name === FOLDER_MANIFEST) continue;
381
+ const rel = relative(dir, p);
382
+ const read = entryFromFile(name, readFileSync(p, "utf8"));
383
+ if (read === null) continue;
384
+ if (read.error) { fail(rel, `${read.error} ${where} holds ${want} entries.`); continue; }
385
+ const kind = read.raw?.kind;
386
+ if (kind === undefined) { warn(rel, `has no "kind", so it is not read as a ${want}. ${where} declares type ${f.type}.`); continue; }
387
+ entries += 1;
388
+ if (kind !== want) { fail(rel, `is a ${kind}, and ${where} declares type ${f.type}, whose entries are ${want}. Move it, or say what the folder holds.`); continue; }
389
+ counts[want] = (counts[want] ?? 0) + 1;
390
+ for (const problem of readEntry(want, read.raw, { owner }).problems) fail(rel, `${want} ${problem}`);
391
+ }
392
+ }
393
+ if (entries === 0) skip("typed entries", `${typed.length} typed folder(s) hold no entry files yet`);
394
+ else ok(entries, "typed entries", `across ${typed.length} folder(s): ${PERSON_KINDS.filter((k) => counts[k]).map((k) => `${counts[k]} ${k}`).join(", ") || "none of the declared kind"}`);
395
+ }
360
396
  }
361
397
 
362
398
  // 13. references (Spec 07, section 4): a pointer, a label and a date, never the payload, never
@@ -406,6 +442,21 @@ export function doctor(dir, { out = console.log, via = "argument" } = {}) {
406
442
  }
407
443
  }
408
444
 
445
+ // Every file under a directory, skipping dot entries, sorted, so a run lists the same order twice.
446
+ function filesUnder(root) {
447
+ const out = [];
448
+ const walk = (d) => {
449
+ for (const name of readdirSync(d).sort()) {
450
+ if (name.startsWith(".")) continue;
451
+ const p = join(d, name);
452
+ if (statSync(p).isDirectory()) walk(p);
453
+ else out.push(p);
454
+ }
455
+ };
456
+ walk(root);
457
+ return out;
458
+ }
459
+
409
460
  function countMd(dir) {
410
461
  try { return mdFiles(dir).length; } catch { return 0; }
411
462
  }