eve-hub 0.16.41 → 0.16.43

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/kit-modules.json CHANGED
@@ -12,11 +12,13 @@
12
12
  "schedules/prune",
13
13
  "schedules/tracker-prs",
14
14
  "schedules/wake",
15
+ "schedules/workspace-review",
15
16
  "skills/deep-dive",
16
17
  "skills/fanout",
17
18
  "skills/forms",
18
19
  "skills/local-onboarding",
19
20
  "skills/onboarding-profile",
21
+ "skills/workspace-review",
20
22
  "tools/apply_tags",
21
23
  "tools/archive_thread",
22
24
  "tools/bash",
package/memory.mjs CHANGED
@@ -27,10 +27,18 @@
27
27
  // The front matter is a small YAML subset: `key: value` or `key: [a, b]`,
28
28
  // with `# comments`. An evidence item is `thr_…/msg_…`, or `msg_…` for
29
29
  // another message in the thread cited just before it.
30
+ //
31
+ // memory/human.md is not an entry: it is the human's profile, one page in the
32
+ // format onboarding/profile.mjs defines. Onboarding writes the first version and
33
+ // the workspace review rewrites it each week, each in a draft PR the human
34
+ // merges. validateMemory checks it and every bot recalls it after the entries.
30
35
  import { existsSync, readFileSync, readdirSync } from "node:fs";
31
36
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
37
+ import { PROFILE_FILE, profileProblems } from "./onboarding/profile.mjs";
32
38
  import { isWorkspace, readWorkspaceFile } from "./workspace.mjs";
33
39
 
40
+ export { PROFILE_FILE };
41
+
34
42
  export const MEMORY_DIR = "memory";
35
43
  export const MAX_RULE_LINES = 3;
36
44
  export const ID = /^[a-z][a-z0-9-]{1,60}$/;
@@ -87,6 +95,26 @@ export function parseEntry(text, file = "entry.md") {
87
95
  return { id: f.id, status: f.status, since: f.since, evidence: f.evidence, ...(supersededBy ? { supersededBy } : {}), rule: lines.join(" ") };
88
96
  }
89
97
 
98
+ // The profile from memory/human.md's text. Throws a MemoryError naming the
99
+ // file and every problem when it's not a valid profile.
100
+ export function parseProfile(text, file = `${MEMORY_DIR}/${PROFILE_FILE}`) {
101
+ const problems = profileProblems(text);
102
+ if (problems.length) throw new MemoryError(`${file}: ${problems.join("; ")}`);
103
+ return { text: text.trim() };
104
+ }
105
+
106
+ // The workspace's profile, or null when memory/human.md doesn't exist, and the
107
+ // error when it isn't valid.
108
+ export function readProfile(root) {
109
+ const path = join(root, MEMORY_DIR, PROFILE_FILE);
110
+ if (!existsSync(path)) return { profile: null, errors: [] };
111
+ try {
112
+ return { profile: parseProfile(readFileSync(path, "utf8")), errors: [] };
113
+ } catch (error) {
114
+ return { profile: null, errors: [error instanceof MemoryError ? error.message : `${MEMORY_DIR}/${PROFILE_FILE}: ${error.message}`] };
115
+ }
116
+ }
117
+
90
118
  // The folders under <root>/memory a bot with these domains recalls, as paths
91
119
  // relative to memory/: "" (the shared root), then each domain and its ancestors.
92
120
  export function domainFolders(domains = []) {
@@ -129,7 +157,7 @@ export function readMemory(root, { label = MEMORY_DIR, knownIds = new Set(), fol
129
157
  if (folder) errors.push(`${prefix}: domain folder doesn't exist`);
130
158
  continue;
131
159
  }
132
- const files = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md") && !d.name.startsWith(".") && d.name.toLowerCase() !== "readme.md").map((d) => d.name).sort();
160
+ const files = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md") && !d.name.startsWith(".") && d.name.toLowerCase() !== "readme.md" && (folder || label !== MEMORY_DIR || d.name !== PROFILE_FILE)).map((d) => d.name).sort();
133
161
  for (const name of files) {
134
162
  const file = `${prefix}/${name}`;
135
163
  try {
@@ -190,10 +218,16 @@ const warnOnce = (msg) => {
190
218
  };
191
219
 
192
220
  // `domains`: the bot's domains (hub.workspace.json), see the header.
221
+ // The profile is recalled after the entries as one message. Its id has a colon,
222
+ // which no entry id (ID) can, so it never replaces a decision with the same id.
223
+ export const PROFILE_ID = "profile:human";
224
+ export const profileText = (profile) => `The human's profile (${MEMORY_DIR}/${PROFILE_FILE}): who they are and how they work, and what the bots do because of it.\n\n${profile.text}`;
225
+
193
226
  export function loadMemoryRecall(root, { warn = warnOnce, domains = /** @type {string[]} */ ([]) } = {}) {
194
227
  const { entries, errors } = readMemory(root, { folders: domainFolders(domains) });
195
- for (const error of errors) warn(error);
196
- return memoryRecall(entries);
228
+ const { profile, errors: profileErrors } = readProfile(root);
229
+ for (const error of [...errors, ...profileErrors]) warn(error);
230
+ return [...memoryRecall(entries), ...(profile ? [{ id: PROFILE_ID, content: profileText(profile) }] : [])];
197
231
  }
198
232
 
199
233
  // A bot's own entries, agents/<bot>/memory/<id>.md: same schema. An id can't
@@ -236,10 +270,11 @@ export function botDomains(root, botDir) {
236
270
  }
237
271
 
238
272
  // For a workspace's tests: throws one MemoryError listing every bad file,
239
- // shared (every folder) and per bot (agents/*/memory/), and every domain a bot
273
+ // shared (every folder), the profile, per bot (agents/*/memory/), and every domain a bot
240
274
  // lists that has no folder. Returns the shared entries.
241
275
  export function validateMemory(root) {
242
276
  const { entries, errors } = readMemory(root, { folders: allMemoryFolders(root) });
277
+ errors.push(...readProfile(root).errors);
243
278
  if (isWorkspace(root)) {
244
279
  const folders = new Set(allMemoryFolders(root));
245
280
  for (const { name, domains = [] } of readWorkspaceFile(root).agents) for (const d of domains) if (!folders.has(d)) errors.push(`hub.workspace.json: ${name} lists domain ${d}, which has no memory/${d}/ folder`);
@@ -10,7 +10,7 @@ The profile is built from many small reads run at the same time, not one pass. Y
10
10
 
11
11
  The result is two files. `read.md` is about one page on how the user works and the team that fits them. `facts.md` is the appendix it cites: the ten questions below, answered from the facts.
12
12
 
13
- Nothing is posted to Slack and nothing is committed by the run itself; its own writes are the pull request in step 6, which the user approves by merging, and a wake-up in this thread for any need that runs once (step 5c), and the first task (below) pushes only once the user says `go` in its thread. The user sees the first message about 20 seconds in, naming the first task, when there is one, that is already being worked on, one message with every Kit approve link if access is pending (step 1), up to four two-sentence findings as the slices finish, one `ask` to confirm what the facts say they would hand off, your answers to anything they ask, and at the end one message with the read, their team and the schedules, followed by one `ask` to pick team changes (step 7). If a step cannot finish, the final message says which and what the user must do; never write the read as if every source had been read.
13
+ Nothing is posted to Slack and nothing is committed by the run itself; its own writes are the pull request in step 6 (the schedule drafts and the first `memory/human.md`), which the user approves by merging, and a wake-up in this thread for any need that runs once (step 5c), and the first task (below) pushes only once the user says `go` in its thread. The user sees the first message about 20 seconds in, naming the first task, when there is one, that is already being worked on, one message with every Kit approve link if access is pending (step 1), up to four two-sentence findings as the slices finish, one `ask` to confirm what the facts say they would hand off, your answers to anything they ask, and at the end one message with the read, their team and the schedules, followed by one `ask` to pick team changes (step 7). If a step cannot finish, the final message says which and what the user must do; never write the read as if every source had been read.
14
14
 
15
15
  The whole run is capped at 25 minutes. Every directory below is under `/tmp/onboarding-skill/`.
16
16
 
@@ -262,29 +262,19 @@ The prompt the bot runs.
262
262
 
263
263
  A schedule file has only `cron`, so it runs every time the cron matches: `0 14 23 11 *` runs every 23 November, not once. Never call a draft one-time. A need that runs once (a pre-freeze check) is not a draft: when you post step 7, arm it with `schedule__wake__create` in this thread, `expression: { type: "single", at, timezone }`, `until` a few minutes after `at`, and the prompt as its note, and list it in the Schedule table with When "once, <date>". If a draft must still end by opening a draft PR that deletes its own file, its prompt and every line of the read and the post that names it say it recurs until that PR is merged and deployed: the schedule keeps running until the deployment without the file is live.
264
264
 
265
- Every run also writes `schedules/main/memory-review.md`, whatever else it found, so the team learns from the user's corrections from the first week:
266
-
267
- ```md
268
- ---
269
- cron: "0 8 * * 1"
270
- ---
271
-
272
- Read every thread from the last 7 days and find each place the human corrected a bot: a rule restated, an answer rejected, work redone their way. For each correction that should outlast its task, write `memory/<id>.md` (or `agents/<bot>/memory/<id>.md` when it is about one bot) with front matter `id`, `status: active`, `since` and `evidence` (the thread and message ids), then the rule in at most 3 lines. When it replaces an entry, set the old one's `status: superseded` and `superseded_by`; never rewrite it. Update `memory/human.md` with what the week showed about who the human is and how they work. Open one draft PR with only those files and never merge it. Post its link in the thread with one line per entry: the rule and the correction it came from. With no corrections, post that and open no PR.
273
- ```
274
-
275
- Its row in the step 7 Schedule table says why it is there in the user's own words when the facts have them: link one or two of the user's corrections (a reply that tells a person or an agent to redo it their way) in "What it reports". With none in the facts, it says it starts from the corrections they make in this workspace.
265
+ main also runs the kit's `workspace-review` schedule (Mondays 08:00 UTC), which every workspace has without a draft: it scores the week, keeps `memory/human.md` and the memory entries current from the user's corrections, proposes bot and schedule changes in a second draft PR, and ranks hub and kit proposals. Its row in the step 7 Schedule table says why it is there in the user's own words when the facts have them: link one or two of the user's corrections (a reply that tells a person or an agent to redo it their way) in "What it reports". With none in the facts, it says it starts from the corrections they make in this workspace.
276
266
 
277
267
  Rules for both files:
278
268
 
279
269
  - Every list item cites at least one URL that is in the facts, or a `fact:` reference: `fact:local/usage-<source>.json#<JSON pointer>` for a value in the Mac's files, `fact:interview/handoff` for the user's pick from the handoff `ask`. No URL or reference appears that isn't in the facts. Never cite a web URL for a local count. State counts as the facts give them, with the window.
280
- - Don't quote messages beyond the snippets, and don't name people beyond what a claim needs. Both files name the user's repos and colleagues, so they are posted only in this thread and never committed.
270
+ - Don't quote messages beyond the snippets, and don't name people beyond what a claim needs. Both files name the user's repos and colleagues, so they are posted only in this thread. The one part committed is the profile, `## Who you are` and `## How you work` copied into `memory/human.md` in step 6, and only to a private workspace repo.
281
271
 
282
272
  ```sh
283
273
  cd /tmp/onboarding-skill && node check-report.mjs --read read.md --profile facts.md --facts facts \
284
274
  --critique critique.md --schedules schedules
285
275
  ```
286
276
 
287
- `--read` checks citations and also fails a trait under `## How you work` that cites fewer than two different sources. `--critique` fails when `critique.md` is missing or holds no `keep`, `cut` or `rewrite` line; it checks presence only, not that the text is verbatim. `--schedules` fails a draft without a five-field `cron`, a missing `main/memory-review.md`, a draft that says it runs once, and a date-pinned or self-deleting draft whose prompt and read lines don't say it recurs until its deletion is merged and deployed. The copy fixes every line the checker prints and reruns until it prints nothing. When it returns, run the checker yourself once more; it must print nothing. Keep `read.md`, `facts.md`, `phases.log` and `critique.md`.
277
+ `--read` checks citations and also fails a trait under `## How you work` that cites fewer than two different sources. `--critique` fails when `critique.md` is missing or holds no `keep`, `cut` or `rewrite` line; it checks presence only, not that the text is verbatim. `--schedules` fails a draft without a five-field `cron`, a draft that says it runs once, and a date-pinned or self-deleting draft whose prompt and read lines don't say it recurs until its deletion is merged and deployed. The copy fixes every line the checker prints and reruns until it prints nothing. When it returns, run the checker yourself once more; it must print nothing. Keep `read.md`, `facts.md`, `phases.log` and `critique.md`.
288
278
 
289
279
  ### Evidence that arrives late
290
280
 
@@ -292,14 +282,22 @@ Late evidence is the user's handoff answer (the whole `## handoff` entry of `fac
292
282
 
293
283
  - **Before 5c starts**, it goes to 5c with the critique's list.
294
284
  - **Once 5c has started**, wait for the read copy to return, then continue it by its `taskId` with only the new evidence. It judges again every trait, `## Your team` row and schedule draft the evidence bears on, including one it contradicts, rewrites them in `read.md`, updates the affected answers in `facts.md`, and returns the list of what it changed. When that list adds or rewrites a trait or a team row, start a fresh `critique` copy (5b) on just those items, giving it the same new evidence (for a handoff answer, the whole `## handoff` entry), and continue the read copy with its list. Run the checker until it prints nothing.
295
- - **Once step 6 has opened its PR**, a refresh that adds, changes or removes a schedule draft changes the PR the same way: write, edit or delete its `agents/<bot>/agent/schedules/<id>.md` on the PR's branch, push, and watch its checks (`gh pr checks`) until they pass, as in step 6. A refresh never drops `memory-review`, so the PR always stays open. Before you touch its branch, check its state (`gh pr view <number> --json state`). If it is `MERGED`, never push to its branch: open a new PR from the default branch with only the changed, added or deleted schedule files, watch its checks the same way, and say the revised schedule is not deployed: the one running is the merged one, and the new one runs only once the user merges the new PR and it deploys. The changed schedule is posted only after its PR passes, or with what failed.
285
+ - **Once step 6 has opened its PR**, a refresh that adds, changes or removes a schedule draft changes the PR the same way: write, edit or delete its `agents/<bot>/agent/schedules/<id>.md` on the PR's branch, push, and watch its checks (`gh pr checks`) until they pass, as in step 6. Before you touch its branch, check its state (`gh pr view <number> --json state`). If it is `MERGED`, never push to its branch: open a new PR from the default branch with only the changed, added or deleted schedule files, watch its checks the same way, and say the revised schedule is not deployed: the one running is the merged one, and the new one runs only once the user merges the new PR and it deploys. The changed schedule is posted only after its PR passes, or with what failed.
296
286
  - **After step 7 has posted**, do the same, then post only what changed: the rewritten lines of the read, the changed team and schedule rows, and a new `ask` if the proposed team changed.
297
287
 
298
288
  The run is over when the user picks from the `ask`. Until then keep `facts/`, because each refresh and its checker need it; delete it then. An answer after that is ordinary conversation, not evidence for the read.
299
289
 
300
290
  ## 6. Open the pull request, if the workspace has a repo
301
291
 
302
- When the workspace is backed by a GitHub repo, open one PR on it that adds only the schedule drafts, as `agents/<bot>/agent/schedules/<id>.md`, and nothing from `read.md` or `facts.md` (`gh pr create`). The user merging it is their approval; the deploy that follows is what makes the schedules run. The PR's checks include the deploy's build, which rejects a bad cron or a bot that does not exist: watch them (`gh pr checks`), fix the draft and push again until they pass, and say in the message if they do not. Without a repo the drafts stay in the thread and the message says so.
292
+ When the workspace is backed by a GitHub repo, open one PR on it that adds the schedule drafts, as `agents/<bot>/agent/schedules/<id>.md`, and the user's profile as `memory/human.md` (`gh pr create`). Nothing else from `read.md` or `facts.md` goes in it.
293
+
294
+ `memory/human.md` is the one profile every bot recalls on every turn, and the weekly workspace review rewrites it from then on, so it has one format, defined in `profile.mjs` beside the checker. Write it as the `## Who you are` and `## How you work` sections of `read.md`, verbatim, and check it:
295
+
296
+ ```sh
297
+ cd /tmp/onboarding-skill && node check-report.mjs --human <repo>/memory/human.md
298
+ ```
299
+
300
+ Commit it only when `gh repo view --json isPrivate --jq .isPrivate` prints `true`, because it names the user's colleagues and repos. When it prints `false`, the command fails, or the repo already has a `memory/human.md`, leave it out and say so in the message. With no drafts and no profile to add, open no PR. The user merging it is their approval; the deploy that follows is what makes the schedules run. The PR's checks include the deploy's build, which rejects a bad cron or a bot that does not exist: watch them (`gh pr checks`), fix the draft and push again until they pass, and say in the message if they do not. Without a repo the drafts stay in the thread and the message says so.
303
301
 
304
302
  ## 7. Post the read, the team and the schedules
305
303
 
@@ -316,7 +314,7 @@ The read in two sentences: who they are and how they work, then the one thing to
316
314
 
317
315
  | Schedule | Bot | When | What it reports |
318
316
  |---|---|---|---|
319
- (one row per draft, `memory-review` for `main` always among them, then one per one-run wake-up)
317
+ (one row per draft, then `workspace-review` for `main`, which the kit already runs, then one per one-run wake-up)
320
318
 
321
319
  [The read](...) and [the facts it cites](...). The drafts are in [this PR](...): merging it turns them on.
322
320
  (one line per source not read or grant declined: what was not read and what to do)
@@ -9,7 +9,9 @@
9
9
  // `fact:interview/<name>` cites the user's own answer, saved under a `## <name>` heading in interview.md in a --facts directory
10
10
  // (the run saves their pick from the handoff `ask` as `## handoff`).
11
11
  // --read read.md as --profile, and every `###` trait under "## How you work" cites at least two different
12
- // sources (Slack, GitHub, Notion, v0, the Mac's files, the user's answers)
12
+ // sources (Slack, GitHub, Notion, v0, the Mac's files, the user's answers), and its
13
+ // "## Who you are" and "## How you work" sections are a valid profile (profile.mjs)
14
+ // --human memory/human.md the file is a valid profile (profile.mjs), the format memory/human.md is recalled in
13
15
  // --phases phases.log the run's step lines are all there and in order: start, quick, first, facts, slices,
14
16
  // areas, read, critique, write, pr, post; finding1 to finding4, each optional, come in
15
17
  // order after first and before read
@@ -18,13 +20,13 @@
18
20
  // reason and the step are good is the writer's judgment, not checked here
19
21
  // --post post.md the first bubble (everything above the first `---` line) is at most two sentences, and
20
22
  // below it are the "Your team" table (| Bot | Change | Why |) and the Schedule table
21
- // (| Schedule | Bot | When | What it reports |), with a `memory-review` row for `main`
23
+ // (| Schedule | Bot | When | What it reports |), with a `workspace-review` row for `main`
22
24
  // --critique critique.md critique.md exists and holds one `keep`, `cut` or `rewrite` line or more; a presence
23
25
  // check only, it cannot prove the text is the critique's verbatim output
24
- // --schedules dir [--read r.md] [--post p.md] every `<bot>/<id>.md` draft has a five-field `cron` and a prompt, and `main/memory-review.md`
25
- // is there. A draft runs every time its cron matches, so none says it runs once; a draft
26
- // whose cron pins a date (day and month) or that deletes its own file says it "recurs until"
27
- // that deletion is "merged and deployed", and so does a line naming it in the read and the post
26
+ // --schedules dir [--read r.md] [--post p.md] every `<bot>/<id>.md` draft has a five-field `cron` and a prompt. A draft runs every
27
+ // time its cron matches, so none says it runs once; a draft whose cron pins a date (day
28
+ // and month) or that deletes its own file says it "recurs until" that deletion is "merged
29
+ // and deployed", and so does a line naming it in the read and the post
28
30
  // Every --facts file is also checked against its own `returned` counts: an object's `returned: { <key>: n }` says its <key> list had n
29
31
  // items when Kit returned it. A slice retypes Kit's output into facts/slack/ and facts/kit/, so a list shorter than its `returned`
30
32
  // means items were dropped while copying, and in those two directories a list of objects with no `returned` count fails too.
@@ -33,6 +35,7 @@
33
35
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
34
36
  import { join, sep } from "node:path";
35
37
  import { pathToFileURL } from "node:url";
38
+ import { profileProblems, profileSections } from "./profile.mjs";
36
39
 
37
40
  const URL_RE = /https?:\/\/[^\s)>\]"'`|]+/g;
38
41
  const FACT_RE = /\bfact:[^\s)>\]"'`]+/g;
@@ -216,7 +219,7 @@ export function postProblems(post) {
216
219
  if (start >= 0) {
217
220
  const rows = [];
218
221
  for (let i = start + 2; i < below.length && /^\s*\|.*\|\s*$/.test(below[i]); i++) rows.push(cells(below[i]));
219
- if (!rows.some(([schedule, bot]) => schedule.replace(/`/g, "") === "memory-review" && bot.replace(/`/g, "") === "main")) problems.push("the Schedule table has no `memory-review` row for `main`; every onboarding offers it");
222
+ if (!rows.some(([schedule, bot]) => schedule.replace(/`/g, "") === "workspace-review" && bot.replace(/`/g, "") === "main")) problems.push("the Schedule table has no `workspace-review` row for `main`; every onboarding offers it");
220
223
  }
221
224
  return problems;
222
225
  }
@@ -232,7 +235,6 @@ const SELF_DELETE = /\bdelet\w*\b[^.\n]*\b(its own|this) (schedule )?file\b/i;
232
235
  // drafts: [["<bot>/<id>.md", text]]; texts: [[name, text]] of the read and the post, whose lines naming a draft are checked too.
233
236
  export function scheduleProblems(drafts, texts = []) {
234
237
  const problems = [];
235
- if (!drafts.some(([f]) => f === "main/memory-review.md")) problems.push("no main/memory-review.md draft; every onboarding offers it");
236
238
  for (const [file, text] of drafts) {
237
239
  const m = /^(\w[\w-]*)\/(\w[\w-]*)\.md$/.exec(file);
238
240
  if (!m) { problems.push(`${file}: a draft is schedules/<bot>/<id>.md`); continue; }
@@ -277,10 +279,11 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
277
279
  for (const p of profiles) {
278
280
  const text = readFileSync(p, "utf8");
279
281
  problems.push(...named(p, citationProblems(text, docs.map(([, d]) => d), files)));
280
- if (reads.includes(p)) problems.push(...named(p, traitProblems(text)));
282
+ if (reads.includes(p)) problems.push(...named(p, [...traitProblems(text), ...profileProblems(profileSections(text))]));
281
283
  if (findings.includes(p)) problems.push(...named(p, findingProblems(text)));
282
284
  }
283
285
  }
286
+ for (const h of after("--human")) problems.push(...profileProblems(readFileSync(h, "utf8")).map((m) => `${h}: ${m}`));
284
287
  const [phases] = after("--phases");
285
288
  if (phases) problems.push(...phaseProblems(readFileSync(phases, "utf8")));
286
289
  const [post] = after("--post");