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.
- package/README.md +37 -5
- package/atris/features/README.md +8 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/commerce/SKILL.md +96 -0
- package/atris/skills/engines/SKILL.md +9 -9
- package/bin/atris.js +12 -0
- package/commands/autoland.js +36 -5
- package/commands/autopilot.js +105 -6
- package/commands/buy.js +242 -0
- package/commands/engine.js +96 -21
- package/commands/host.js +56 -4
- package/commands/land.js +129 -8
- package/commands/member.js +68 -14
- package/commands/mission.js +116 -4
- package/commands/one-lap.js +14 -10
- package/commands/stream.js +56 -22
- package/commands/task.js +7 -0
- package/commands/team.js +82 -10
- package/commands/transactions.js +115 -0
- package/commands/worktree.js +39 -0
- package/commands/youtube.js +58 -26
- package/lib/commerce.js +75 -0
- package/lib/engine-ask.js +112 -9
- package/lib/engine-registry.js +300 -39
- package/lib/fleet.js +444 -26
- package/lib/functional-owner.js +15 -6
- package/lib/host.js +296 -170
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +25 -1
- package/lib/member-park.js +138 -0
- package/lib/member-scaffold.js +39 -0
- package/lib/member-type.js +27 -0
- package/lib/roster-markdown.js +1 -1
- package/lib/roster-prep.js +289 -0
- package/lib/roster-runs.js +389 -0
- package/lib/roster-suggest.js +145 -0
- package/lib/router-brain.js +111 -3
- package/lib/runner-command.js +37 -2
- package/lib/self-drive.js +26 -2
- package/lib/state-detection.js +3 -3
- package/lib/task-db.js +38 -3
- package/lib/team-lineup.js +7 -2
- package/lib/todo.js +1 -1
- package/lib/wish-audit.js +1 -1
- package/package.json +1 -1
- package/scripts/det/cpu-hotspots.js +216 -0
- package/scripts/det/ytnotes +169 -23
- package/scripts/det/ytquote-repair.js +18 -4
- package/scripts/det/ytrail-bench.js +214 -0
- package/scripts/det/ytrail-eval.js +128 -56
- package/templates/members/host/MEMBER.md +5 -3
- package/templates/members/host/MISSION.md +1 -1
- 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:
|
|
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 = '
|
|
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(
|
|
65
|
+
return String(text).replace(/^/, '').split(/\r?\n/, 1)[0] || '';
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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:
|
|
93
|
-
transcriptWords:
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
fs.
|
|
111
|
-
|
|
104
|
+
function readIfExists(file) {
|
|
105
|
+
return file && fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
106
|
+
}
|
|
112
107
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
@@ -1,16 +1,64 @@
|
|
|
1
|
-
# Host
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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.
|