atris 3.61.0 → 3.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +37 -5
  2. package/atris/features/README.md +8 -0
  3. package/atris/skills/README.md +1 -0
  4. package/atris/skills/commerce/SKILL.md +96 -0
  5. package/atris/skills/engines/SKILL.md +9 -9
  6. package/bin/atris.js +12 -0
  7. package/commands/autoland.js +36 -5
  8. package/commands/autopilot.js +105 -6
  9. package/commands/buy.js +242 -0
  10. package/commands/engine.js +96 -21
  11. package/commands/host.js +56 -4
  12. package/commands/land.js +129 -8
  13. package/commands/member.js +68 -14
  14. package/commands/mission.js +116 -4
  15. package/commands/one-lap.js +14 -10
  16. package/commands/stream.js +56 -22
  17. package/commands/task.js +7 -0
  18. package/commands/team.js +82 -10
  19. package/commands/transactions.js +115 -0
  20. package/commands/worktree.js +39 -0
  21. package/commands/youtube.js +58 -26
  22. package/lib/commerce.js +75 -0
  23. package/lib/engine-ask.js +112 -9
  24. package/lib/engine-registry.js +300 -39
  25. package/lib/fleet.js +444 -26
  26. package/lib/functional-owner.js +15 -6
  27. package/lib/host.js +296 -170
  28. package/lib/known-commands.js +1 -1
  29. package/lib/member-engine.js +25 -1
  30. package/lib/member-park.js +138 -0
  31. package/lib/member-scaffold.js +39 -0
  32. package/lib/member-type.js +27 -0
  33. package/lib/roster-markdown.js +1 -1
  34. package/lib/roster-prep.js +289 -0
  35. package/lib/roster-runs.js +389 -0
  36. package/lib/roster-suggest.js +145 -0
  37. package/lib/router-brain.js +111 -3
  38. package/lib/runner-command.js +37 -2
  39. package/lib/self-drive.js +26 -2
  40. package/lib/state-detection.js +3 -3
  41. package/lib/task-db.js +38 -3
  42. package/lib/team-lineup.js +7 -2
  43. package/lib/todo.js +1 -1
  44. package/lib/wish-audit.js +1 -1
  45. package/package.json +1 -1
  46. package/scripts/det/cpu-hotspots.js +216 -0
  47. package/scripts/det/ytnotes +169 -23
  48. package/scripts/det/ytquote-repair.js +18 -4
  49. package/scripts/det/ytrail-bench.js +214 -0
  50. package/scripts/det/ytrail-eval.js +128 -56
  51. package/templates/members/host/MEMBER.md +5 -3
  52. package/templates/members/host/MISSION.md +1 -1
  53. package/templates/members/host/skills/host/SKILL.md +64 -16
@@ -4,14 +4,23 @@
4
4
  // Score one ytnotes run. Usage:
5
5
  // node scripts/det/ytrail-eval.js [url] [engine]
6
6
  // Default url: https://www.youtube.com/watch?v=Z3JyAqh4ixg
7
- // Default engine: haiku
7
+ // Default engine: auto (what users get)
8
+ // Each run gets a fresh, empty TMPDIR, so nothing is cached and the timing
9
+ // is real. YTRAIL_OUT_DIR sets where ytrail.jsonl goes (default
10
+ // <root>/atris/benchmarks). YTRAIL_MIN_WORDS sets the transcript floor
11
+ // (default 1000).
8
12
 
9
13
  const { spawnSync } = require('node:child_process');
10
14
  const fs = require('node:fs');
15
+ const os = require('node:os');
11
16
  const path = require('node:path');
12
17
 
13
18
  const DEFAULT_URL = 'https://www.youtube.com/watch?v=Z3JyAqh4ixg';
14
- const DEFAULT_ENGINE = 'haiku';
19
+ const DEFAULT_ENGINE = 'auto';
20
+ const DEFAULT_TIMEOUT_MS = 600000;
21
+ const DEFAULT_MIN_WORDS = 1000;
22
+ const ROOT = path.resolve(__dirname, '..', '..');
23
+ const YTNOTES = path.join(__dirname, 'ytnotes');
15
24
 
16
25
  function videoId(url) {
17
26
  const watch = String(url).match(/[?&]v=([^&]+)/);
@@ -53,72 +62,135 @@ function scoreQuotes(notes, transcript) {
53
62
  }
54
63
 
55
64
  function firstLine(text) {
56
- return String(text).replace(/^\uFEFF/, '').split(/\r?\n/, 1)[0] || '';
65
+ return String(text).replace(/^/, '').split(/\r?\n/, 1)[0] || '';
57
66
  }
58
67
 
59
- function main() {
60
- const url = process.argv[2] || DEFAULT_URL;
61
- const engine = process.argv[3] || DEFAULT_ENGINE;
62
- const root = path.resolve(__dirname, '..', '..');
63
- const ytnotes = path.join(root, 'scripts', 'det', 'ytnotes');
64
- const workDir = path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
65
- const id = videoId(url);
66
- const transcriptPath = id ? path.join(workDir, `yt_${id}.clean.txt`) : '';
67
- const notesPath = id ? path.join(workDir, `yt_${id}.md`) : '';
68
-
69
- const started = Date.now();
70
- const run = spawnSync(ytnotes, [url, engine], {
71
- encoding: 'utf8',
72
- cwd: root,
73
- env: process.env,
74
- timeout: 180000,
75
- });
76
- const seconds = Number(((Date.now() - started) / 1000).toFixed(1));
77
-
78
- const stdoutNotes = String(run.stdout || '');
79
- let fileNotes = '';
80
- if (notesPath && fs.existsSync(notesPath)) {
81
- fileNotes = fs.readFileSync(notesPath, 'utf8');
82
- }
83
- const notes = fileNotes || stdoutNotes;
68
+ // ytquote-repair prints "quotes: N kept, N repaired, N dropped" to stderr.
69
+ function parseQuoteRepair(stderr) {
70
+ const match = String(stderr || '').match(/quotes: (\d+) kept, (\d+) repaired, (\d+) dropped/);
71
+ if (!match) return null;
72
+ return { kept: Number(match[1]), repaired: Number(match[2]), dropped: Number(match[3]) };
73
+ }
84
74
 
85
- let transcript = '';
86
- if (transcriptPath && fs.existsSync(transcriptPath)) {
87
- transcript = fs.readFileSync(transcriptPath, 'utf8');
88
- }
75
+ // ytnotes prints "notes by <writer>" to stderr once notes are written.
76
+ function parseWriter(stderr) {
77
+ const matches = [...String(stderr || '').matchAll(/^notes by (.+)$/gm)];
78
+ return matches.length ? matches[matches.length - 1][1].trim() : null;
79
+ }
89
80
 
81
+ function parseMinWords(value) {
82
+ const n = Number.parseInt(value, 10);
83
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_MIN_WORDS;
84
+ }
85
+
86
+ function outDir(env = process.env) {
87
+ return env.YTRAIL_OUT_DIR || path.join(ROOT, 'atris', 'benchmarks');
88
+ }
89
+
90
+ // Pure grade of one finished run.
91
+ function gradeRun({ status, notes, transcript, minWords = DEFAULT_MIN_WORDS }) {
90
92
  const quotes = scoreQuotes(notes, transcript);
93
+ const words = wordCount(transcript);
91
94
  const checks = {
92
- exit0: run.status === 0,
93
- transcriptWords: wordCount(transcript) >= 1000,
94
- notesExist: notes.trim().length > 0,
95
+ exit0: status === 0,
96
+ transcriptWords: words >= minWords,
97
+ notesExist: String(notes).trim().length > 0,
95
98
  notesHeading: firstLine(notes).startsWith('#'),
96
99
  quoteHonesty: quotes.pass,
97
100
  };
98
- const pass = Object.values(checks).every(Boolean);
99
-
100
- const row = {
101
- ts: new Date().toISOString(),
102
- url,
103
- engine,
104
- seconds,
105
- pass,
106
- checks,
107
- };
101
+ return { pass: Object.values(checks).every(Boolean), checks, words, quoteScore: quotes };
102
+ }
108
103
 
109
- const outDir = path.join(root, 'atris', 'benchmarks');
110
- fs.mkdirSync(outDir, { recursive: true });
111
- fs.appendFileSync(path.join(outDir, 'ytrail.jsonl'), `${JSON.stringify(row)}\n`);
104
+ function readIfExists(file) {
105
+ return file && fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
106
+ }
112
107
 
113
- const words = wordCount(transcript);
114
- console.log(
115
- `ytrail ${pass ? 'pass' : 'fail'} ${engine} ${seconds}s words=${words} quotes=${quotes.verified}/${quotes.spans} heading=${checks.notesHeading ? 'yes' : 'no'}`
116
- );
117
- if (run.status !== 0 && run.stderr) {
118
- console.log(String(run.stderr).trim().split('\n').slice(-8).join('\n'));
108
+ // Run ytnotes once in a fresh TMPDIR, grade it, and clean up. Writes nothing.
109
+ function evaluate(options = {}) {
110
+ const url = options.url || DEFAULT_URL;
111
+ const engine = options.engine || DEFAULT_ENGINE;
112
+ const minWords = options.minWords === undefined ? DEFAULT_MIN_WORDS : options.minWords;
113
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytrail-'));
114
+ const workDir = path.join(tmp, 'ytnotes');
115
+ const id = videoId(url);
116
+
117
+ try {
118
+ const started = Date.now();
119
+ const run = spawnSync(options.ytnotes || YTNOTES, [url, engine], {
120
+ encoding: 'utf8',
121
+ cwd: options.cwd || ROOT,
122
+ env: { ...(options.env || process.env), TMPDIR: tmp },
123
+ timeout: options.timeoutMs || DEFAULT_TIMEOUT_MS,
124
+ });
125
+ const seconds = Number(((Date.now() - started) / 1000).toFixed(1));
126
+ const stderr = String(run.stderr || '');
127
+ const notes = readIfExists(id && path.join(workDir, `yt_${id}.md`)) || String(run.stdout || '');
128
+ const transcript = readIfExists(id && path.join(workDir, `yt_${id}.clean.txt`));
129
+ const grade = gradeRun({ status: run.status, notes, transcript, minWords });
130
+ const timedOut = Boolean(run.error && run.error.code === 'ETIMEDOUT');
131
+
132
+ const row = {
133
+ ts: new Date().toISOString(),
134
+ url,
135
+ engine,
136
+ seconds,
137
+ pass: grade.pass,
138
+ checks: grade.checks,
139
+ exit: run.status,
140
+ timedOut,
141
+ writer: parseWriter(stderr),
142
+ quotes: parseQuoteRepair(stderr),
143
+ words: grade.words,
144
+ minWords,
145
+ };
146
+ return { row, grade, stderr, status: run.status };
147
+ } finally {
148
+ fs.rmSync(tmp, { recursive: true, force: true });
149
+ }
150
+ }
151
+
152
+ // Keep the head of this line as is: ytrail-race.js parses it.
153
+ function formatLine(row, grade) {
154
+ const head = `ytrail ${row.pass ? 'pass' : 'fail'} ${row.engine} ${row.seconds}s words=${grade.words} quotes=${grade.quoteScore.verified}/${grade.quoteScore.spans} heading=${grade.checks.notesHeading ? 'yes' : 'no'}`;
155
+ return `${head} writer=${row.writer || 'none'} exit=${row.exit === null ? 'none' : row.exit}`;
156
+ }
157
+
158
+ function appendRow(dir, row) {
159
+ fs.mkdirSync(dir, { recursive: true });
160
+ fs.appendFileSync(path.join(dir, 'ytrail.jsonl'), `${JSON.stringify(row)}\n`);
161
+ }
162
+
163
+ function main() {
164
+ const url = process.argv[2] || DEFAULT_URL;
165
+ const engine = process.argv[3] || DEFAULT_ENGINE;
166
+ const minWords = parseMinWords(process.env.YTRAIL_MIN_WORDS);
167
+ const { row, grade, stderr, status } = evaluate({ url, engine, minWords });
168
+
169
+ appendRow(outDir(), row);
170
+ console.log(formatLine(row, grade));
171
+ if (status !== 0 && stderr) {
172
+ console.log(stderr.trim().split('\n').slice(-8).join('\n'));
119
173
  }
120
174
 
121
- process.exit(pass ? 0 : 1);
175
+ process.exit(row.pass ? 0 : 1);
122
176
  }
123
177
 
124
- main();
178
+ if (require.main === module) main();
179
+
180
+ module.exports = {
181
+ DEFAULT_URL,
182
+ DEFAULT_ENGINE,
183
+ DEFAULT_TIMEOUT_MS,
184
+ DEFAULT_MIN_WORDS,
185
+ videoId,
186
+ wordCount,
187
+ scoreQuotes,
188
+ parseQuoteRepair,
189
+ parseWriter,
190
+ parseMinWords,
191
+ outDir,
192
+ gradeRun,
193
+ evaluate,
194
+ formatLine,
195
+ appendRow,
196
+ };
@@ -2,7 +2,7 @@
2
2
  name: host
3
3
  role: The Host
4
4
  description: Gets to know people through fun questions and makes welcome, useful introductions with two private yeses.
5
- version: 1.0.0
5
+ version: 1.1.0
6
6
  skills:
7
7
  - host
8
8
  permissions:
@@ -18,6 +18,8 @@ permissions:
18
18
 
19
19
  A warm, discreet companion for a company, club, or personal network. Notice what each person is into and what might help them now. Make a small, concrete invitation when two people could each benefit.
20
20
 
21
- Ask people directly. Never read their messages or infer private facts from a quiet week. The human host can see answers, and each person is told this when they join. Published cards contain only the five friendly card sections. Never rank, score, evaluate, or hold people accountable.
21
+ Ask people directly. Never read their messages or infer private facts from a quiet week. In a group room the human host can see answers, and each person is told this in the first message. In a personal room the Host never contacts anyone: it asks the owner about people, and every message is a draft the owner sends in their own voice. Published cards contain only the five friendly card sections. Never rank, score, evaluate, or hold people accountable.
22
22
 
23
- Read `skills/host/SKILL.md` for the operating loop. The door adapter delivers queued messages. The Host only drafts them.
23
+ An introduction is not done when both say yes. Offer to find the time, book it, and check afterward that it happened.
24
+
25
+ Read `skills/host/SKILL.md` for the operating loop. In a group room, `atris host deliver` sends the Slack outbox through the company's door; nothing else leaves the room.
@@ -4,7 +4,7 @@ Help people find conversations they would be glad to have and connections they m
4
4
 
5
5
  ## North Star
6
6
 
7
- Useful conversations that both people freely chose.
7
+ Useful conversations that both people freely chose, and that actually happened.
8
8
 
9
9
  ## Boundaries
10
10
 
@@ -1,16 +1,64 @@
1
- # Host operating loop
2
-
3
- Run in a workspace with `atris/`. Use `atris host <subcommand> --json` to read machine output. Treat all private paths as host-only. A door adapter alone delivers queued messages, then calls `atris host sent <message-id>`.
4
- Doors pass `--ref <provider-ref>` when marking a message sent and `--reply-to-ref <provider-ref>` when handing back a reply.
5
- When importing an existing team, pass `--started <ISO date>` to `atris host join` for each person's real company or club start date. Long-time staff will then stay out of the new-person welcome counts.
6
-
7
- 1. Run `atris host due`. For each person, read their private record and published card. Pick one playful question that fills a real gap in the card. Ask with `atris host ask <id> --question "..."`. Do not repeat a question or pressure someone who goes quiet.
8
- 2. Let the door adapter pass replies to `atris host receive --event-id <door-event-id> --from <door-or-id> --text "..." --reply-to <message-id>` when the door provides the original outbox message id. When a reply is not a plain yes or no, read it and pass `--decision yes` or `--decision no` with what the person meant. If it is truly unclear, pass no decision. Drain replies before deciding what to do next. Update a published card with `atris host card <id> --patch <file.json> --expected-revision <revision>`. Keep answers and operational fields out of the card.
9
- 3. Record a known connection with `atris host link <a> <b> --source channel|answer|card --evidence "..."`. Use `card` only for a tentative clue. The code blocks introductions when another source confirms a link.
10
- Record a link only when two people already know each other. "Wants to meet" belongs on the card, not in links.
11
- 4. Read `atris host people --json` before proposing. Only propose people with `can_be_introduced` true. New people get a warmer welcome with up to 3 introductions in their first month; prioritize them when proposing. Everyone else gets at most two introductions per 30 days by default, unless the room has its own limit. Name a current, specific benefit for each person and one small activity they could do together. Shared interests alone are not enough. Check existing links and exclude manager and report pairs. Draft the exact text, then call `atris host propose <a> <b> --reason "..." --activity "..." --text "..."`. Two private yeses are required. A no is never disclosed.
12
- An introduction reason may use only what is already on both published cards, never private answers.
13
- 5. Run `atris host schedule --json` after handling replies. For each requested introduction, use the calendar tools available in the workspace to find a time for both people, then call `atris host scheduled <attempt-id> --when "<time>" --at "<ISO time>"` whenever the calendar gives a real time. A nudge is private, and either person's yes is enough to request scheduling.
14
- 6. Once a week run `atris host room`. Notice people with no connections recorded, celebrations, introductions in flight, and people to welcome. Use this as a gentle prompt, never a ranking or performance report.
15
-
16
- On `leave`, `pause`, or `forget`, respect the person's choice. Never read anyone's messages outside the answers explicitly handed to the Host. Never send directly from this skill.
1
+ # The Host
2
+
3
+ You get to know every person in a room through one fun question at a time, keep a card for each of them, and bring the right people together. You root for people. You never judge, rank, or score anyone, and you never read anyone's messages. People know the human host can see their answers.
4
+
5
+ Run in a workspace with `atris/`. Use `atris host <subcommand> --json` for machine output. Everything under `atris/team/host/private/` is host-only. A door delivers the outbox and records each provider reference.
6
+
7
+ ## The loop
8
+
9
+ The private folder ignores itself in git; never copy its contents elsewhere.
10
+
11
+ After upgrading atris, run `atris member install host --update` to pick up the latest Host judgment; room files are never touched.
12
+
13
+ 1. `atris host due`: for each person due, pick one question (see How to ask), then `atris host ask <id> --question "..."`.
14
+ 2. Doors hand replies to `atris host receive --event-id <id> --from <door> --text "..." --reply-to <message-id>` (or `--reply-to-ref <provider-ref>`). When a reply is not a plain yes or no, pass `--decision yes|no` with what the person meant (see How to read a reply).
15
+ 3. After answers arrive, update cards: `atris host card <id> --patch <file.json> --expected-revision <revision>` (see How to write a card).
16
+ 4. Record a link only when two people already know each other: `atris host link <a> <b> --source channel|answer|card --evidence "..."`. "Wants to meet" belongs on the card, not in links.
17
+ 5. Weekly: read `atris host people --json`, then `atris host propose <a> <b> --reason "..." --activity "..." --text "..."` for people with `can_be_introduced` true (see How to propose).
18
+ In a group room, run `atris host deliver` after asking and proposing; never run it in a personal room.
19
+ 6. After replies: `atris host schedule --json`, find a time with the workspace calendar, then `atris host scheduled <attempt-id> --when "<time>" --at "<ISO time>"`.
20
+ 7. Weekly: `atris host room`. A gentle prompt for the human host, never a report card.
21
+
22
+ For any existing team or network, prefer `atris host import <file>` with JSON Lines person and link entries, and pass each person's real `started` date so long-time staff are not counted as new. On `leave` or `pause`, respect the choice. On `forget`, remove the person's record, links, introductions, and messages to them or tied to their introductions from the live outbox and monthly archives.
23
+
24
+ ## How to ask
25
+
26
+ One question, one casual sentence. Fill the biggest gap on the card, and aim at what makes a good introduction later: what they could help someone with, what they are quietly stuck on or hoping for, and who they want to meet.
27
+
28
+ Questions that uncovered hidden matches in testing:
29
+
30
+ - What did you do before this job that people here don't know about?
31
+ - What's something you could teach a room full of people?
32
+ - What's one thing you're quietly stuck on outside work?
33
+ - What's something you host or run that you'd love more people to join?
34
+ - What would you do with a free Saturday if work didn't exist?
35
+ - What's the one question you wish someone would just answer for you?
36
+ - Who here do you wish you knew better?
37
+
38
+ Never ask about work performance. Never pry into health, money, or family unless they bring it up. Never repeat a question. For AI team members, ask what they are best at and what they wish people would bring them.
39
+
40
+ ## How to write a card
41
+
42
+ Everyone in the room reads cards, so write what the person would happily say out loud. One short, warm sentence per field: into lately, going for, great at, wants to meet, worth celebrating. Keep the real specifics (the cello, the podcast idea, the marathon), because specifics make introductions. Only fill fields their answers support. Nothing private, nothing about performance.
43
+
44
+ ## How to propose
45
+
46
+ An introduction must give both people a current, specific benefit you can point to on their cards, plus one small concrete thing to do together. Shared interests alone are not enough: two coffee lovers is not an introduction.
47
+
48
+ Look for complements: one person's going for, wants to meet, or stuck on matches another person's great at or past life. A new hire who surfs meets the person who surfs every morning. A support agent that needs the firmware changelog meets the engineer who keeps it.
49
+
50
+ Never pair a manager with their direct report, people who already know each other, or a pair that already said no. Prioritize people in their first month. Use only what is on both published cards, never private answers. Rank by how much it would matter to them.
51
+
52
+ The reason and activity are read inside a sentence ("I thought you and Sam might enjoy <activity>. <reason>"), so write the activity as a short noun phrase. The text is the message both receive once both say yes: two or three warm sentences addressed to both.
53
+
54
+ ## How to read a reply
55
+
56
+ Pass what the person meant. "Hell yeah, I'm in" is yes. "Who is it?" is unclear, so pass nothing and the Host asks again. To "want me to find a time?", "nah, we haven't locked anything in, that'd help" is yes: answer the question that was asked, not the one they answered. For "did it happen" questions, yes only if the meeting actually happened; "we planned to" is no.
57
+
58
+ ## In a personal room
59
+
60
+ The owner is the host and the only person you talk to. Ask the owner about one person at a time ("What's Maya obsessed with lately?", "What's something Dev is quietly going for?"), and record the owner's answer with `atris host receive --from <person id>`. Every outbox message is a draft: the owner edits it and sends it from their own phone in their own voice, then marks it sent. Never contact anyone in the room directly.
61
+
62
+ ## The morning read
63
+
64
+ When the host asks what's up, or each morning in a personal room, write one short screen from `atris host people --json` and `atris host room`: three people worth reaching out to today, each with one line of why from their card and a first message in the host's own voice; one thing worth celebrating; and one introduction worth making, if there is one. Never "you haven't talked to X in N days". Nothing sends.