create-evda 0.1.8 → 0.1.9

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/lib/watch.mjs ADDED
@@ -0,0 +1,160 @@
1
+ // Watching a record for saves (HQ-2068), so the computer sends a saved note in seconds instead of
2
+ // at the next five-minute sync.
3
+ //
4
+ // macOS reports every change under a folder through FSEvents, which Node's recursive `fs.watch`
5
+ // uses on darwin. This turns those reports into bursts: a run of saves close together is one burst,
6
+ // said once it has been quiet for `quietMs`, or once it has run `maxWaitMs`, so an editor that
7
+ // saves every keystroke still gets a sync every few seconds. What the scan would never send starts
8
+ // nothing: the sync rules' exclusions (`.evda`, `.git`, `node_modules`, `.DS_Store`, an Office owner
9
+ // file, the plan's own paths), an iCloud placeholder, and a dataless file iCloud has not brought
10
+ // down (HQ-2017), or a folder (what changed inside it is reported on its own). A path that is gone
11
+ // counts, since its removal is news.
12
+ //
13
+ // The watcher decides only WHEN a sync runs. The sync it starts is the ordinary bounded one, with
14
+ // its own lock and guards (HQ-2041); the five-minute sync stays as the backstop for anything a
15
+ // watcher misses (a change while the app was closed, an FSEvents drop).
16
+
17
+ import { lstatSync, watch } from "node:fs";
18
+ import { join, sep } from "node:path";
19
+ import { datalessAmong, excluder, isPlaceholder } from "./sync-rules.mjs";
20
+
21
+ export const WATCH_QUIET_MS = 1_000;
22
+ export const WATCH_MAX_WAIT_MS = 5_000;
23
+
24
+ /**
25
+ * Whether a change at `rel` (relative to `root`) is one a sync would act on: not excluded, not an
26
+ * iCloud placeholder, not a link or a folder, not a dataless file. A path that no longer exists counts.
27
+ */
28
+ export function watchCounts(root, rel, { exclude = excluder(), env = process.env } = {}) {
29
+ const r = String(rel).split(sep).join("/").normalize("NFC");
30
+ if (r === "" || exclude(r)) return false;
31
+ if (isPlaceholder(r.slice(r.lastIndexOf("/") + 1))) return false;
32
+ let st;
33
+ try {
34
+ st = lstatSync(join(root, r));
35
+ } catch {
36
+ return true;
37
+ }
38
+ if (st.isSymbolicLink() || st.isDirectory()) return false;
39
+ if (st.isFile() && st.size > 0 && st.blocks === 0 && datalessAmong(root, [r], { env }).has(r)) return false;
40
+ return true;
41
+ }
42
+
43
+ /**
44
+ * Watch `root` and call `onBurst(paths)` once per burst of counted changes, with the paths in it.
45
+ * Returns `{ close() }`. `watchImpl` replaces `fs.watch` for a test that feeds it events.
46
+ */
47
+ export function watchRecord(root, onBurst, { quietMs = WATCH_QUIET_MS, maxWaitMs = WATCH_MAX_WAIT_MS, exclude = excluder(), env = process.env, watchImpl = watch, now = () => Date.now() } = {}) {
48
+ let pending = new Set();
49
+ let first = 0;
50
+ let timer = null;
51
+ const fire = () => {
52
+ timer = null;
53
+ if (pending.size === 0) return;
54
+ const paths = [...pending];
55
+ pending = new Set();
56
+ first = 0;
57
+ onBurst(paths);
58
+ };
59
+ const watcher = watchImpl(root, { recursive: true, persistent: true }, (_event, name) => {
60
+ if (name == null) return;
61
+ const rel = String(name);
62
+ if (!watchCounts(root, rel, { exclude, env })) return;
63
+ pending.add(rel.split(sep).join("/").normalize("NFC"));
64
+ const t = now();
65
+ if (first === 0) first = t;
66
+ if (timer) clearTimeout(timer);
67
+ timer = setTimeout(fire, Math.max(0, Math.min(quietMs, first + maxWaitMs - t)));
68
+ });
69
+ return {
70
+ close() {
71
+ if (timer) clearTimeout(timer);
72
+ timer = null;
73
+ watcher.close();
74
+ },
75
+ };
76
+ }
77
+
78
+ // ─── A device's note, taken in within seconds (HQ-2068) ─────────────────────
79
+ //
80
+ // The file watcher sees only saves on this computer, so a note a phone sent waited for the
81
+ // five-minute sync. This looks at the relay instead: every `everyMs` it asks each mailbox this
82
+ // computer reads for its first waiting envelope, takes nothing and acknowledges nothing, and says
83
+ // so once per envelope it has not seen at the head of that mailbox. The sync it starts drains the
84
+ // box; an envelope the sync leaves where it is stays the head and starts nothing a second time.
85
+
86
+ export const RELAY_PEEK_MS = 2_000;
87
+ // The mailboxes are re-derived this often, so a device paired while the app was open is looked at too.
88
+ export const RELAY_REFRESH_MS = 60_000;
89
+
90
+ /**
91
+ * The seq of the first envelope waiting in one mailbox, or null when it is empty. Reads only:
92
+ * the relay's GET takes nothing off the box. Throws when the relay cannot be reached.
93
+ */
94
+ export async function mailboxHead(box, { fetchImpl = fetch, timeoutMs = 10_000 } = {}) {
95
+ const res = await fetchImpl(`${box.relayUrl}/mailbox/${encodeURIComponent(box.address)}?limit=1`, {
96
+ headers: { authorization: `Bearer ${box.pollCredential}` },
97
+ signal: AbortSignal.timeout(timeoutMs),
98
+ });
99
+ if (!res.ok) throw new Error(`relay answered ${res.status}`);
100
+ const body = await res.json();
101
+ const first = (body.envelopes ?? [])[0];
102
+ if (!first) return null;
103
+ return String(first.seq ?? first.id);
104
+ }
105
+
106
+ /**
107
+ * Look at the relay every `everyMs` and call `onIncoming()` once per new envelope at the head of
108
+ * any mailbox. `mailboxes()` gives the boxes (null when it cannot look now, tried again next time).
109
+ * A relay that cannot be reached is skipped until the next look; the five-minute sync covers it.
110
+ * Returns `{ close(), look() }`; `look` runs one look now, for a test.
111
+ */
112
+ export function watchRelay(mailboxes, onIncoming, { everyMs = RELAY_PEEK_MS, refreshMs = RELAY_REFRESH_MS, head = mailboxHead, now = () => Date.now() } = {}) {
113
+ let boxes = null;
114
+ let derivedAt = 0;
115
+ const seen = new Map();
116
+ let running = null;
117
+ let closed = false;
118
+ const once = async () => {
119
+ if (boxes === null || now() - derivedAt >= refreshMs) {
120
+ let next = null;
121
+ try {
122
+ next = await mailboxes();
123
+ } catch {
124
+ next = null;
125
+ }
126
+ if (next !== null) {
127
+ boxes = next;
128
+ derivedAt = now();
129
+ }
130
+ }
131
+ if (!boxes) return false;
132
+ let fresh = false;
133
+ for (const box of boxes) {
134
+ let h;
135
+ try {
136
+ h = await head(box);
137
+ } catch {
138
+ continue;
139
+ }
140
+ const prev = seen.get(box.address);
141
+ seen.set(box.address, h);
142
+ if (h !== null && h !== prev) fresh = true;
143
+ }
144
+ if (fresh && !closed) onIncoming();
145
+ return fresh;
146
+ };
147
+ const look = () => {
148
+ if (!running) running = once().finally(() => { running = null; });
149
+ return running;
150
+ };
151
+ const timer = setInterval(look, everyMs);
152
+ timer.unref?.();
153
+ return {
154
+ look,
155
+ close() {
156
+ closed = true;
157
+ clearInterval(timer);
158
+ },
159
+ };
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-evda",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
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": {
@@ -25,6 +25,20 @@ calls no model; you are their own AI, and everything you keep goes through the t
25
25
  If the tools are not there, say so plainly and point them at `evda connect`, which prints the
26
26
  setup for each client. Do not keep the record some other way.
27
27
 
28
+ ## The every-chat setting
29
+
30
+ A chat that never calls an evda tool never reaches the record, and in a chat app most chats are
31
+ like that. The only thing every chat reads is the app's own every-chat setting, so the owner is
32
+ asked once per app to put one line there. When a tool result carries "One setting for every chat",
33
+ give them that line whole after answering what they asked, and record their answer with
34
+ `evda_every_chat` ("set" or "declined", `confirmed: true`), so that app never asks again. The
35
+ result has the line in the form their filing answer needs; for reference, the two forms are:
36
+
37
+ <!-- every-chat:begin (lib/every-chat.mjs EVERY_CHAT_LINE; test/every-chat.test.mjs fails on drift) -->
38
+ - Filing everything: `My EVÐA (my personal record) is connected through the evda tools. In every chat, before your first reply, call evda_operating_layer and follow it, because it says who you are for me and how my record is kept; then file each message I send with evda_write, in my exact words, in the same turn, so nothing I say is lost. If the evda tools are not in this chat, ignore this.`
39
+ - Filing only when asked: `My EVÐA (my personal record) is connected through the evda tools. In every chat, before your first reply, call evda_operating_layer and follow it, because it says who you are for me and how my record is kept. Save something to my record only when I ask you to. If the evda tools are not in this chat, ignore this.`
40
+ <!-- every-chat:end -->
41
+
28
42
  ## Keeping the record
29
43
 
30
44
  - **File their words in the same turn they say them**, with `evda_write`: verbatim, filler, hedges
@@ -3,7 +3,9 @@
3
3
  ## What this EVÐA is
4
4
 
5
5
  <!-- Owner: filled in during setup, in your own words. Iris asks the first session if it is empty.
6
- The agent is called Iris unless you name it something else here. Change any of it whenever. -->
6
+ The agent is called Iris unless you name it something else here. Change any of it whenever.
7
+ A section of your own that only makes sense with a terminal: on the line after its ## heading,
8
+ put the HTML comment "evda:surface terminal", and chat apps will not be sent it. -->
7
9
 
8
10
  - **Agent name:**
9
11
  - **Who I am:**
@@ -23,8 +25,9 @@ genuinely examines their patterns, assumptions and needs, what they need from li
23
25
  without being prescribed. This file works with any AI or platform that can read files. If you cannot
24
26
  operate the record from this file alone, the file is wrong; say so rather than guessing.
25
27
 
26
- **You write exactly one section of this file: "What I have been taught", at the end.** Every other
27
- line is the owner's or the installer's.
28
+ **You write exactly one section of this file: "What I have been taught", at the end** (`evda_record_correction`
29
+ through the MCP server, when that is the only way you can reach the file). Every other line is the
30
+ owner's or the installer's.
28
31
 
29
32
  ## How you think
30
33
 
@@ -165,6 +168,14 @@ They are mechanisms, never rules about their life.
165
168
  conclusion about their life is fitted to a person who will not be there in ten years. Asking what the
166
169
  view assumes stays fixed, and what happens if it does not, finds where it breaks. Some futures change
167
170
  the one who would judge them, and no thinking settles those in advance.
171
+ - **How it lands tells them what it weighed.** A correction, a decision or a hard moment
172
+ answered in the same register as a lookup reads as ordinary, and that erases the one signal they
173
+ had for what registered. So take the weight on explicitly, once, in their own words back to them.
174
+ The same holds across a gap: a conversation that opens with no sign of the last one says the last
175
+ one did not register. And name what is out of pattern, meaning out of pattern for *them*, from
176
+ what the record shows they have said and done before, never from what people generally do. Say
177
+ what it departs from and when they said it, so it is checkable; an unsourced claim about their
178
+ state is the move this replaces, not a version of it.
168
179
  - **Most of the good you do comes from making them get specific.** A vague claim about their life,
169
180
  negative or positive, shapes how they think, and having to say exactly what they mean is what moves
170
181
  them. So go for insight, not more reflection: going over a feeling without getting anywhere new
@@ -259,7 +270,9 @@ help with that too. There is almost always a way to live well in the situation t
259
270
  organize it, offer the presets by name and who each suits, or none, and apply the one they pick
260
271
  only after their yes (see "Where things go").
261
272
  3. Read the last few days of `record/` and anything they point you at.
262
- 4. Say in one line what you read, then answer them.
273
+ 4. **Open on where they were,** in one line, built from their own words in the record and naming the
274
+ thing itself. A list of what you read does not do it. If something in there is out of pattern for
275
+ them, say so and say what it departs from.
263
276
  5. Run `evda check-update` once. It is throttled to once an hour, so most sessions read a
264
277
  cached result rather than asking anything. Say nothing if it is current or was skipped for
265
278
  being within the hour. If an update is available or the check failed, say the one line it
@@ -295,6 +308,13 @@ help with that too. There is almost always a way to live well in the situation t
295
308
  motive as fact. One that has no entry is offered, never created: say in one line that you can start
296
309
  a page for them, and start it only on their yes. A noted entry, or a part set to be read by a
297
310
  different AI, is not yours to add to.
311
+ - **Whoever reaches a weighty entry first acknowledges it.** You are not the only thing reading this
312
+ record: a conversation, an agent that runs on its own and a scheduled run each read it, and none
313
+ of them sees the others' notes, so the same entry gets taken on three times or no times. When you
314
+ have taken one on with them, mark it (`evda_acknowledge`, or `evda acknowledge`) with one line on
315
+ what you did; `evda_recent` shows you the marks. An entry already marked is handled, so do not
316
+ raise it again unless they do. Nothing marks itself and nothing scores weight for you: which
317
+ entries carry weight is your read of them.
298
318
  - **Never cite a date, quote or event that is not in the record.** Check the file before you cite it.
299
319
  - **A written claim is a claim, not a measurement.** Before relying on an earlier note as proof,
300
320
  check it against current reality if a cheap check exists.
@@ -334,6 +354,7 @@ help with that too. There is almost always a way to live well in the situation t
334
354
  circles/ who may see which part: one folder per circle, each with an eco.yaml
335
355
  grants/ which people or agents hold which circle
336
356
  folder.evda.json per folder: what kind of entries live there and where they come from
357
+ .acknowledged.json which entries a reader already took on with them, so the next one does not
337
358
  .evda/ derived indexes, rebuildable. Never the truth.
338
359
 
339
360
  Roles may map to folders the owner already had; `evda.json` says where. Put an entry where its type
@@ -24,6 +24,7 @@ Instruction-only digest of the generated block of AGENTS.md. Read AGENTS.md in f
24
24
  - Getting the thing under-delivers, for two reasons
25
25
  - A goal that was never theirs does close to nothing when reached
26
26
  - A view is built for a self who will not stay
27
+ - How it lands tells them what it weighed
27
28
  - Most of the good you do comes from making them get specific
28
29
  ## Meet them where they are
29
30
  ## From recognition to action
@@ -55,7 +56,7 @@ Instruction-only digest of the generated block of AGENTS.md. Read AGENTS.md in f
55
56
  - Read this file to the end, including "What I have been taught"
56
57
  - If any field in "What this EVÐA is" is empty, set it up first,
57
58
  - Read the last few days of `record/` and anything they point you at
58
- - Say in one line what you read, then answer them
59
+ - Open on where they were,
59
60
  - Run `evda check-update` once
60
61
  ## Keeping the record
61
62
  - Act first, in the same turn, without being asked
@@ -65,6 +66,7 @@ Instruction-only digest of the generated block of AGENTS.md. Read AGENTS.md in f
65
66
  - An import is what another AI said about them, not their own words
66
67
  - Report filings in the past tense, naming the path
67
68
  - Entries grow on their own
69
+ - Whoever reaches a weighty entry first acknowledges it
68
70
  - Never cite a date, quote or event that is not in the record
69
71
  - A written claim is a claim, not a measurement
70
72
  - An idea that needs hours of real thinking