atris 3.62.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.
@@ -86,6 +86,14 @@ None.
86
86
 
87
87
  ### Completed Features
88
88
 
89
+ #### youtube-notes
90
+ Free, fast, checked notes from any YouTube link
91
+ - **Files:** commands/youtube.js, scripts/det/ytnotes, scripts/det/ytquote-repair.js, test/ytnotes.test.js, test/ytquote-repair.test.js, test/youtube.test.js, atris/features/youtube-notes/*
92
+ - **Status:** complete; the `atris youtube bench` scorecard is in review
93
+ - **Keywords:** youtube, notes, captions, transcript, whisper, quotes
94
+ - **What:** `atris youtube notes <url>` reads the real English captions, transcribes on an Apple silicon Mac when there are none, writes with the fastest installed AI, and checks every quote against the transcript
95
+ - **Completed:** 2026-09-28
96
+
89
97
  #### audit-gaps
90
98
  Close remaining audit gaps from self-audit
91
99
  - **Files:** atris/team/*/MEMBER.md, atris/features/README.md, atris/features/audit-gaps/*
@@ -34,6 +34,7 @@ cp -r atris/skills/[name] ~/.codex/skills/
34
34
  | backend | Backend architecture policy | `policies/atris-backend.md` |
35
35
  | blocks | Author Atris block documents: docs, decks, reports | - |
36
36
  | calendar | Google Calendar via AtrisOS API | - |
37
+ | commerce | Quote a purchase or stock order; the person approves before anything is charged | - |
37
38
  | copy-editor | Detects and fixes AI writing patterns | - |
38
39
  | create-app | Build and deploy an Atris app from a description | - |
39
40
  | create-member | Create and manage MEMBER.md team members | - |
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: commerce
3
+ description: "Buy or trade through Atris for a person: get a quote, show it, the person approves, check status, read their transactions. Use when asked to buy tickets, buy an item from a store, place a stock order, or list what someone bought or traded. Triggers on buy, purchase, order, trade, stock order, transactions, what did I spend."
4
+ version: 1.0.0
5
+ tags:
6
+ - commerce
7
+ - money
8
+ ---
9
+
10
+ # Commerce: buy or trade through Atris
11
+
12
+ You can quote and ask. Only the human can pay. Every dollar leaves only after
13
+ the person acts themselves: a tap in their Link app, a payment page they
14
+ fill in, or a text from their own phone.
15
+
16
+ ## The loop
17
+
18
+ 1. Quote. `atris buy quote --kind <kind> [fields]`.
19
+ 2. Show the person the summary, total, fee and approval text. Word for word on money.
20
+ 3. Only after they say yes, approve. `atris buy approve <id> [fields]`.
21
+ 4. Hand the person the approval link (Link or payment page). For trades, tell them to check their texts: Atris sends the one-time code only to their own text thread, never to you. Nothing else pays.
22
+ 5. Check `atris buy status <id>` until the state is `done`, `failed`, `refunded` or `unknown`.
23
+ 6. `atris transactions` lists everything they bought, paid in fees, traded or sent.
24
+
25
+ Add `--json` to any command for the raw response.
26
+
27
+ ## Kinds
28
+
29
+ | kind | quote flags | approve flags | human approves by |
30
+ |---|---|---|---|
31
+ | `ticket` | `--event-url` (vividseats.com), `--quantity`, `--min-each-usd`, `--max-each-usd` | `--pick` (1-3 or ticket_id), `--delivery-email` | Link app, or payment page |
32
+ | `shop` | `--query` or `--url`, `--quantity` | `--options '<json>'`, `--delivery-email`, `--address '<json>'` (US, once), `--accept-extras` | Link app |
33
+ | `trade` | `--side` buy/sell, `--symbol`, `--quantity` or `--dollars`, `--order-type` market/limit, `--limit-price` | none | replying to Atris's text with the exact phrase from their own phone, within 5 minutes |
34
+ | `flight` | not wired yet, quotes are refused | | |
35
+
36
+ Any field can also go in as JSON: `--intent '{"kind":"shop","query":"navy tee"}'` for quotes,
37
+ `--details '{"pick":"1"}'` for approve. Flags win over the JSON.
38
+
39
+ ## Rails (enforced in code, not by you)
40
+
41
+ - Approval is always human. An API key cannot confirm a trade or approve a Link spend.
42
+ - Caps: purchases up to `TEXT_TICKETS_MAX_USD` (default $400) and $500 per Link spend. Stocks: per-order and per-day caps, the Robinhood risk policy, market hours, no shorting, stocks only.
43
+ - Quotes expire: 30 minutes for purchases, 10 for trades. A trade quote stages at most one order. Ask again after that.
44
+ - A trade already waiting for the person's yes blocks a new one. Let them confirm or let it expire.
45
+ - No retries. If a result is `unknown`, money may have moved. Tell the person to check the store or Robinhood app. Never buy again to "fix" it.
46
+ - No advice on trades. Carry out only the person's own instruction.
47
+ - For Link-mode orders, status may carry `fee_payment_url`: the page where the person pays the Atris fee. The CLI prints it as "Pay the Atris fee".
48
+ - Status values from quote and approve: `quoted`, `pending_approval`, `needs_input` (ask the person for the named field), `needs_setup` (send them the connect link), `refused` (read the message to them).
49
+
50
+ ## With the CLI
51
+
52
+ ```bash
53
+ # Tickets
54
+ atris buy quote --kind ticket --event-url "https://www.vividseats.com/..." --quantity 2
55
+ atris buy approve ord_<uuid> --pick 1 --delivery-email sam@example.com
56
+ # -> Approve in Link: https://... Nothing has been charged yet.
57
+
58
+ # A store item
59
+ atris buy quote --kind shop --query "uniqlo supima crew tee navy" --quantity 1
60
+
61
+ # A stock order
62
+ atris buy quote --kind trade --side buy --symbol VTI --dollars 25
63
+ atris buy approve trq_<token>
64
+ # -> We texted you a confirm code. Reply to that text to place the trade.
65
+
66
+ # Status and history
67
+ atris buy status ord_<uuid>
68
+ atris transactions --since 2026-09-01 --kind ticket,shop,trade --limit 50
69
+ # Quotes nobody approved are left out; add --include-quotes to see them.
70
+ ```
71
+
72
+ ## With curl
73
+
74
+ ```bash
75
+ KEY="Authorization: Bearer $ATRIS_API_KEY"
76
+ API=https://api.atris.ai/api
77
+
78
+ curl -s -X POST $API/commerce/quote -H "$KEY" -H 'content-type: application/json' \
79
+ -d '{"kind":"ticket","event_url":"https://www.vividseats.com/...","quantity":2}'
80
+ curl -s -X POST $API/commerce/ord_<uuid>/approve -H "$KEY" -H 'content-type: application/json' \
81
+ -d '{"pick":"1","delivery_email":"sam@example.com"}'
82
+ # -> {"status":"pending_approval","approval":{"how":"link_app","url":"https://..."}}
83
+
84
+ curl -s -X POST $API/commerce/quote -H "$KEY" -H 'content-type: application/json' \
85
+ -d '{"kind":"trade","side":"buy","symbol":"VTI","dollars":25}'
86
+ curl -s -X POST $API/commerce/trq_<token>/approve -H "$KEY"
87
+ # -> {"status":"pending_approval","approval":{"how":"text_confirm","text":"Check your texts. ..."}}
88
+
89
+ curl -s $API/commerce/ord_<uuid> -H "$KEY"
90
+ curl -s "$API/transactions?since=2026-09-01&kinds=ticket,shop,trade&limit=50" -H "$KEY"
91
+ # Quotes nobody approved are left out; add &include_quotes=true to see them.
92
+ ```
93
+
94
+ Each transaction: `id, kind, when, what, amount_cents, currency, state, counterparty, receipt_url`.
95
+ `sources` says which ledgers loaded; if one says `failed`, the list is partial. Say so.
96
+ The CLI prints that as "Partial list: could not load ...".
package/bin/atris.js CHANGED
@@ -717,6 +717,8 @@ function showHelpAll() {
717
717
  console.log(' usage - Show developer API usage');
718
718
  console.log(' api-key - Create, list, rotate, or revoke a developer API key');
719
719
  console.log(' topup - Buy credits and print a Stripe checkout URL');
720
+ console.log(' buy - Quote a purchase or stock order; the person approves before anything is charged');
721
+ console.log(' transactions - List what you bought, paid in fees, traded, or sent');
720
722
  console.log(' design - Extract a site design system, check brand adherence, search brands');
721
723
  console.log(' mcp - Run the atris MCP server (stdio) for Claude Desktop and Cursor');
722
724
  console.log('');
@@ -2368,6 +2370,15 @@ if (command === 'guide') {
2368
2370
  Promise.resolve(require('../commands/topup').topupCommand(process.argv.slice(3)))
2369
2371
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
2370
2372
  .catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
2373
+ } else if (command === 'buy') {
2374
+ // Buy: quote, then hand the person the approval step. Never charges.
2375
+ Promise.resolve(require('../commands/buy').buyCommand(process.argv.slice(3)))
2376
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2377
+ .catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
2378
+ } else if (command === 'transactions') {
2379
+ Promise.resolve(require('../commands/transactions').transactionsCommand(process.argv.slice(3)))
2380
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2381
+ .catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
2371
2382
  } else if (command === '_resolve') {
2372
2383
  // Hidden: resolve a profile name query → print exact profile name
2373
2384
  require('../commands/auth').resolveProfile();
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ // atris buy: quote a purchase or trade, start the person's approval, check status.
4
+ //
5
+ // atris buy quote --kind ticket --event-url <vividseats url> --quantity 2
6
+ // atris buy approve ord_<id> --pick 1 --delivery-email sam@example.com
7
+ // atris buy status ord_<id>
8
+ //
9
+ // Nothing is ever charged by this command. Approve only starts the human step:
10
+ // a tap in their Link app, a payment page, or a reply to the confirm code Atris
11
+ // texts to their own phone. The code never comes back to this command.
12
+
13
+ const { apiRequestJson } = require('../utils/api');
14
+ const { loadCredentials } = require('../utils/auth');
15
+ const { NOT_LOGGED_IN, tokenFrom, oneLine, printResult } = require('../lib/developer-api');
16
+ const { parseFlags, formatCents, untilText, errorFrom } = require('../lib/commerce');
17
+
18
+ const KINDS = ['ticket', 'shop', 'trade', 'flight'];
19
+ const NOTHING_CHARGED = 'Nothing is charged until the person approves it themselves.';
20
+ const CONTROL_FLAGS = new Set(['json', 'help', 'intent', 'details']);
21
+ const TRADE_CONFIRM = 'We texted you a confirm code. Reply to that text to place the trade.';
22
+
23
+ function showBuyHelp(log = console.log) {
24
+ log(`usage: atris buy <quote|approve|status> [flags] [--json]
25
+
26
+ get a real quote, then hand the person the approval step. nothing is charged
27
+ until the person approves it themselves (Link app, payment page, or a text
28
+ from their own phone).
29
+
30
+ atris buy quote --kind ticket|shop|trade|flight [fields] [--intent '<json>']
31
+ atris buy approve <id> [fields] [--details '<json>']
32
+ atris buy status <id>
33
+
34
+ quote fields (flags, kebab-case is fine):
35
+ ticket --event-url <vividseats.com url> --quantity N [--min-each-usd N] [--max-each-usd N]
36
+ shop --query "<item>" | --url <product url> [--quantity N]
37
+ trade --side buy|sell --symbol VTI (--quantity N | --dollars N) [--order-type market|limit --limit-price N]
38
+ flight not wired yet, quotes are refused
39
+
40
+ approve fields:
41
+ ticket --pick 1 --delivery-email you@example.com
42
+ shop --delivery-email you@example.com [--options '<json>'] [--address '<json>'] [--accept-extras]
43
+ trade none. Atris texts the person a confirm code; they reply to that text.
44
+
45
+ examples:
46
+ atris buy quote --kind trade --side buy --symbol VTI --dollars 25
47
+ atris buy quote --intent '{"kind":"shop","query":"uniqlo supima crew tee navy"}'
48
+ atris buy approve ord_1234 --pick 1 --delivery-email sam@example.com
49
+ atris buy status ord_1234 --json`);
50
+ }
51
+
52
+ function fieldsFrom(flags) {
53
+ const out = {};
54
+ for (const [key, value] of Object.entries(flags)) {
55
+ if (!CONTROL_FLAGS.has(key)) out[key] = value;
56
+ }
57
+ return out;
58
+ }
59
+
60
+ function jsonFlag(flags, name) {
61
+ if (flags[name] == null) return { ok: true, value: {} };
62
+ const value = flags[name];
63
+ if (value && typeof value === 'object' && !Array.isArray(value)) return { ok: true, value };
64
+ return { ok: false, error: `--${name} must be a JSON object, like --${name} '{"kind":"shop","query":"navy tee"}'` };
65
+ }
66
+
67
+ function approvalLines(approval) {
68
+ if (!approval || typeof approval !== 'object') return [];
69
+ const lines = [];
70
+ // The backend sends trade confirm codes only to the person's own texts.
71
+ if (approval.how === 'text_confirm') return [TRADE_CONFIRM];
72
+ if (approval.url) {
73
+ const label = approval.how === 'connect_link' ? 'Connect Link first'
74
+ : approval.how === 'payment_page' ? 'Pay on this page'
75
+ : 'Approve in Link';
76
+ lines.push(`${label}: ${approval.url}`);
77
+ }
78
+ if (approval.text) lines.push(approval.text);
79
+ return lines;
80
+ }
81
+
82
+ function moneyLines(data) {
83
+ const lines = [];
84
+ if (data.total_cents != null) lines.push(`Total: ${formatCents(data.total_cents, data.currency)}`);
85
+ if (data.fee_cents != null) lines.push(`Atris fee: ${formatCents(data.fee_cents, data.currency)}`);
86
+ return lines;
87
+ }
88
+
89
+ function renderQuote(data = {}, now = Date.now()) {
90
+ const lines = [];
91
+ if (data.status !== 'quoted') return renderOutcome(data, now);
92
+ lines.push(data.summary || 'Quote ready.');
93
+ lines.push('');
94
+ lines.push(...moneyLines(data));
95
+ if (data.expires_at) lines.push(`Expires: ${data.expires_at} (${untilText(data.expires_at, now)})`);
96
+ if (data.id) lines.push(`Quote id: ${data.id}`);
97
+ if (data.product_url) lines.push(`Product: ${data.product_url}`);
98
+ const choices = data.options && typeof data.options === 'object' && !Array.isArray(data.options)
99
+ ? Object.entries(data.options) : [];
100
+ if (choices.length) {
101
+ lines.push('Choices:');
102
+ for (const [name, values] of choices) {
103
+ lines.push(` ${name}: ${Array.isArray(values) ? values.join(', ') : oneLine(values)}`);
104
+ }
105
+ }
106
+ if (data.note) lines.push('', String(data.note));
107
+ lines.push('');
108
+ lines.push(NOTHING_CHARGED);
109
+ const approval = data.approval && data.approval.text ? data.approval.text : '';
110
+ if (approval) lines.push(approval);
111
+ if (data.id) {
112
+ const hints = data.approve_with && typeof data.approve_with === 'object'
113
+ ? Object.keys(data.approve_with).map((key) => `--${key.replace(/_/g, '-')} <${key}>`).join(' ')
114
+ : '';
115
+ lines.push(`Once they say yes: atris buy approve ${data.id}${hints ? ` ${hints}` : ''}`);
116
+ }
117
+ return lines.join('\n');
118
+ }
119
+
120
+ function renderOutcome(data = {}, now = Date.now()) {
121
+ const lines = [];
122
+ const status = String(data.status || '');
123
+ if (status === 'pending_approval') {
124
+ lines.push(data.message || 'Waiting for the person to approve.');
125
+ lines.push('');
126
+ lines.push(...moneyLines(data));
127
+ if (data.expires_at) lines.push(`Expires: ${data.expires_at} (${untilText(data.expires_at, now)})`);
128
+ if (data.id) lines.push(`Id: ${data.id}`);
129
+ lines.push('');
130
+ lines.push('Next, the person does this themselves:');
131
+ for (const line of approvalLines(data.approval)) lines.push(` ${line}`);
132
+ if (data.fee_card && data.fee_card.url) lines.push(` Fee card: ${data.fee_card.url}`);
133
+ lines.push('');
134
+ lines.push('Nothing has been charged yet. Money moves only after the person acts themselves.');
135
+ if (data.id) lines.push(`Check it: atris buy status ${data.id}`);
136
+ return lines.join('\n');
137
+ }
138
+ if (status === 'needs_setup') {
139
+ lines.push(data.message || 'Setup needed first.');
140
+ for (const line of approvalLines(data.approval)) lines.push(` ${line}`);
141
+ lines.push('Nothing was charged.');
142
+ return lines.join('\n');
143
+ }
144
+ if (status === 'needs_input') {
145
+ lines.push(data.message || 'More details needed.');
146
+ if (Array.isArray(data.fields) && data.fields.length) lines.push(`Needed: ${data.fields.join(', ')}`);
147
+ if (data.extra_usd != null) lines.push(`Extra: $${data.extra_usd}`);
148
+ if (data.total_usd != null) lines.push(`New total: $${data.total_usd}`);
149
+ lines.push('Nothing was charged.');
150
+ return lines.join('\n');
151
+ }
152
+ if (status === 'refused') {
153
+ lines.push(data.message || 'That did not go through.');
154
+ if (!/nothing was/i.test(String(data.message || ''))) lines.push('Nothing was charged.');
155
+ return lines.join('\n');
156
+ }
157
+ return data.message || oneLine(data);
158
+ }
159
+
160
+ function renderStatus(data = {}) {
161
+ const lines = [];
162
+ lines.push(`${data.id || ''} ${data.kind || ''} ${String(data.state || 'unknown').toUpperCase()}`.trim());
163
+ if (data.what) lines.push(`What: ${data.what}`);
164
+ if (data.amount_cents != null) lines.push(`Amount: ${formatCents(data.amount_cents, data.currency)}`);
165
+ if (data.when) lines.push(`When: ${data.when}`);
166
+ if (data.detail) lines.push(`Detail: ${data.detail}`);
167
+ if (data.counterparty) lines.push(`With: ${data.counterparty}`);
168
+ if (data.receipt_url) lines.push(`Receipt: ${data.receipt_url}`);
169
+ if (data.fee_payment_url) lines.push(`Pay the Atris fee: ${data.fee_payment_url}`);
170
+ const approval = approvalLines(data.approval);
171
+ if (approval.length) {
172
+ lines.push('Waiting on the person:');
173
+ for (const line of approval) lines.push(` ${line}`);
174
+ }
175
+ if (data.state === 'unknown') lines.push('Money may have moved. Check the store or Robinhood app. Do not buy again to fix it.');
176
+ return lines.join('\n');
177
+ }
178
+
179
+ function buildRequest(sub, pos, flags) {
180
+ if (sub === 'quote') {
181
+ const base = jsonFlag(flags, 'intent');
182
+ if (!base.ok) return base;
183
+ const body = { ...base.value, ...fieldsFrom(flags) };
184
+ const kind = String(body.kind || '').toLowerCase();
185
+ if (!KINDS.includes(kind)) return { ok: false, error: `--kind must be one of ${KINDS.join(', ')}` };
186
+ body.kind = kind;
187
+ return { ok: true, path: '/commerce/quote', method: 'POST', body };
188
+ }
189
+ if (sub === 'approve' || sub === 'status') {
190
+ const id = String(pos[1] || '').trim();
191
+ if (!id) return { ok: false, error: `usage: atris buy ${sub} <id>` };
192
+ const path = `/commerce/${encodeURIComponent(id)}`;
193
+ if (sub === 'status') return { ok: true, path, method: 'GET' };
194
+ const base = jsonFlag(flags, 'details');
195
+ if (!base.ok) return base;
196
+ return { ok: true, path: `${path}/approve`, method: 'POST', body: { ...base.value, ...fieldsFrom(flags) } };
197
+ }
198
+ return { ok: false, error: 'usage: atris buy <quote|approve|status>. run: atris buy --help' };
199
+ }
200
+
201
+ async function buyCommand(args = [], deps = {}) {
202
+ const io = { log: deps.log || console.log, err: deps.err || console.error };
203
+ const { flags, pos } = parseFlags(args);
204
+ const sub = String(pos[0] || '').toLowerCase();
205
+ if (!sub || sub === 'help' || flags.help) {
206
+ showBuyHelp(io.log);
207
+ return 0;
208
+ }
209
+
210
+ const json = flags.json === true;
211
+ const fail = (error, status) => {
212
+ printResult({ json, ok: false, error, payload: { ok: false, error, ...(status ? { status } : {}) } }, io);
213
+ return 1;
214
+ };
215
+
216
+ const req = buildRequest(sub, pos, flags);
217
+ if (!req.ok) return fail(req.error);
218
+
219
+ const load = deps.loadCredentials || loadCredentials;
220
+ const request = deps.apiRequestJson || apiRequestJson;
221
+ const token = tokenFrom(load());
222
+ if (!token) return fail(NOT_LOGGED_IN);
223
+
224
+ try {
225
+ // No retries: a quote or approval must never be sent twice by accident.
226
+ const options = { method: req.method, token, retries: 0 };
227
+ if (req.body) options.body = req.body;
228
+ const result = await request(req.path, options);
229
+ if (!result || !result.ok) return fail(errorFrom(result), result && result.status);
230
+ const data = result.data || {};
231
+ const now = deps.now ? deps.now() : Date.now();
232
+ const text = sub === 'quote' ? renderQuote(data, now)
233
+ : sub === 'approve' ? renderOutcome(data, now)
234
+ : renderStatus(data);
235
+ printResult({ json, ok: true, text, payload: { ok: true, ...data } }, io);
236
+ return 0;
237
+ } catch (error) {
238
+ return fail(oneLine(error && error.message));
239
+ }
240
+ }
241
+
242
+ module.exports = { buyCommand };
@@ -7,6 +7,7 @@ const { loadCredentials } = require('../utils/auth');
7
7
  const { resolveBackendRoot } = require('../utils/backend-root');
8
8
  const { apiRequestJson } = require('../utils/api');
9
9
  const { runAliveTick } = require('../lib/member-alive');
10
+ const { memberRefusalReason } = require('../lib/member-type');
10
11
  const { defaultObjectiveRunner } = require('../lib/default-runner');
11
12
  const { readJson, writeJson } = require('../lib/json-file');
12
13
  const { sleepSync } = require('../lib/sleep-sync');
@@ -306,6 +307,28 @@ function nearestMemberSlug(name, slugs) {
306
307
  return bestScore <= Math.max(2, Math.floor(needle.length / 3)) ? best : null;
307
308
  }
308
309
 
310
+ // A person is never run as an agent. Reads the member's own type and returns
311
+ // the plain refusal sentence, or '' when the member can run.
312
+ function memberRefusalFor(name, memberFile) {
313
+ try {
314
+ return memberRefusalReason(name, parseFrontmatter(fs.readFileSync(memberFile, 'utf8')) || {});
315
+ } catch {
316
+ return '';
317
+ }
318
+ }
319
+
320
+ // Same as requireMemberDir, then stops with a non-zero exit for a human or
321
+ // odd-typed member. Every path that runs a member as an agent starts here.
322
+ function requireRunnableMember(name) {
323
+ const paths = requireMemberDir(name);
324
+ const reason = memberRefusalFor(name, paths.memberFile);
325
+ if (reason) {
326
+ console.error(reason);
327
+ process.exit(1);
328
+ }
329
+ return paths;
330
+ }
331
+
309
332
  function requireMemberDir(name) {
310
333
  if (!name) {
311
334
  console.error('Usage: atris member <goal|tick|review> <name> ...');
@@ -1077,7 +1100,7 @@ function memberRun(name, ...args) {
1077
1100
  }
1078
1101
 
1079
1102
  const asJson = hasFlag(args, '--json');
1080
- const paths = requireMemberDir(name);
1103
+ const paths = requireRunnableMember(name);
1081
1104
  // Parking only hides a member from the team views; a run by name still goes.
1082
1105
  const { isParkedText } = require('../lib/member-park');
1083
1106
  try {
@@ -4221,6 +4244,8 @@ name: ${name}
4221
4244
  role: ${role}
4222
4245
  description: ${description || `Handles ${role.toLowerCase()} tasks`}
4223
4246
  version: 1.0.0
4247
+ # ai or human; a human member is a person and Atris never acts as them
4248
+ type: ai
4224
4249
 
4225
4250
  skills: []
4226
4251
 
@@ -7863,6 +7888,8 @@ function wakeMemberEngine(name, root = process.cwd()) {
7863
7888
 
7864
7889
  async function runMemberWake(name, { execute = false, confirmed = false, force = false, domainInput = {} } = {}) {
7865
7890
  const paths = requireMemberDir(name);
7891
+ const refusal = memberRefusalFor(name, paths.memberFile);
7892
+ if (refusal) throw new Error(refusal);
7866
7893
  const runtimeKind = paths.runtimeKind || memberRuntimeKind(name);
7867
7894
  if (runtimeKind === 'auto-improver') {
7868
7895
  return runAutoImproverWake(name, paths, { execute, confirmed });
@@ -8176,6 +8203,7 @@ function clipText(value, max = 70) {
8176
8203
  }
8177
8204
 
8178
8205
  async function memberWake(name, ...args) {
8206
+ if (name) requireRunnableMember(name);
8179
8207
  const asJson = hasFlag(args, '--json');
8180
8208
  const execute = hasFlag(args, '--execute') && !hasFlag(args, '--dry-run');
8181
8209
  const confirmed = hasFlag(args, '--confirm-autonomy-policy');
@@ -8223,14 +8251,15 @@ function collectFleetActivationRows(root = process.cwd()) {
8223
8251
  const goalPlane = memberGoalPlaneStatus(slug, paths);
8224
8252
  const activeMission = memberLiveMission(slug, missionMap);
8225
8253
  const latestMission = memberLatestMission(slug, missionMap);
8226
- const skipped = !goalPlane.goal && !activeMission;
8254
+ const refusal = memberRefusalFor(slug, paths.memberFile);
8255
+ const skipped = Boolean(refusal) || (!goalPlane.goal && !activeMission);
8227
8256
  return {
8228
8257
  member: slug,
8229
8258
  goal: goalPlane.goal || null,
8230
8259
  active_mission: activeMission || null,
8231
8260
  latest_mission: latestMission || null,
8232
8261
  skipped,
8233
- skip_reason: skipped ? 'no_goal_and_no_active_mission' : null,
8262
+ skip_reason: refusal ? 'not_ai' : skipped ? 'no_goal_and_no_active_mission' : null,
8234
8263
  };
8235
8264
  })
8236
8265
  .sort((a, b) => a.member.localeCompare(b.member));
@@ -8263,7 +8292,7 @@ async function memberLoopAll(kind, ...args) {
8263
8292
  for (const row of rows) {
8264
8293
  if (row.skipped) {
8265
8294
  skipped.push(row);
8266
- if (!asJson) console.log(`Skip ${row.member}: no goal and no active mission.`);
8295
+ if (!asJson) console.log(`Skip ${row.member}: ${row.skip_reason === 'not_ai' ? 'not an AI member.' : 'no goal and no active mission.'}`);
8267
8296
  continue;
8268
8297
  }
8269
8298
  if (!asJson) console.log(`Activate ${row.member}: ${row.goal ? 'goal' : `mission ${row.active_mission?.id || 'active'}`}`);
@@ -8675,7 +8704,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
8675
8704
  // Driver: the phases above do the work; this reads as setup -> gate -> run -> record.
8676
8705
  async function memberLoop(name, ...args) {
8677
8706
  if (name === '--all' || name === 'all') return memberLoopAll('loop', ...args);
8678
- requireMemberDir(name);
8707
+ requireRunnableMember(name);
8679
8708
  const asJson = hasFlag(args, '--json');
8680
8709
  const aliveMode = hasFlag(args, '--alive');
8681
8710
  const execute = hasFlag(args, '--execute');
@@ -8739,7 +8768,7 @@ async function memberLoop(name, ...args) {
8739
8768
  }
8740
8769
 
8741
8770
  async function memberTick(name, ...args) {
8742
- const paths = requireMemberDir(name);
8771
+ const paths = requireRunnableMember(name);
8743
8772
  const asJson = hasFlag(args, '--json');
8744
8773
  const force = hasFlag(args, '--force');
8745
8774
  const goalId = readFlag(args, '--goal', '');
@@ -22,6 +22,7 @@ const {
22
22
  const {
23
23
  resolveClaudeRunnerModel,
24
24
  resolvePinnedRunnerModel,
25
+ runnerModelFor,
25
26
  resolveClaudeRunnerBin,
26
27
  resolveClaudeRunnerCommandTemplate,
27
28
  buildRunnerCommand,
@@ -568,6 +569,8 @@ function applyMissionRunnerProfile(runner) {
568
569
  // default model pin. Every other engine's CLI rejects a claude model name, so
569
570
  // it only ever takes an explicit pin (the mission's model or the roster
570
571
  // line's); '' lets the engine ride its own default.
572
+ // A mission often still carries the claude model it was created with; an
573
+ // engine that runs only its own models (codex) drops it.
571
574
  const CLAUDE_FAMILY_RUNNER_ENGINES = new Set(['claude', 'fable', 'haiku']);
572
575
 
573
576
  function resolveMissionTickRunnerModel(mission) {
@@ -575,7 +578,7 @@ function resolveMissionTickRunnerModel(mission) {
575
578
  if (!engine || CLAUDE_FAMILY_RUNNER_ENGINES.has(engine)) {
576
579
  return resolveClaudeRunnerModel(mission);
577
580
  }
578
- return resolvePinnedRunnerModel(mission);
581
+ return runnerModelFor(engine, resolvePinnedRunnerModel(mission));
579
582
  }
580
583
 
581
584
  function exitMissionError(message, code = 1, asJson = false) {
package/commands/task.js CHANGED
@@ -7922,6 +7922,8 @@ function publicWords(value) {
7922
7922
  ]).has(word));
7923
7923
  }
7924
7924
 
7925
+ const { memberTypeOf } = require('../lib/member-type');
7926
+
7925
7927
  function parseMemberFrontmatter(text) {
7926
7928
  const source = String(text || '');
7927
7929
  if (!source.startsWith('---')) return {};
@@ -7952,6 +7954,7 @@ function readTeamMembers(root = process.cwd()) {
7952
7954
  slug,
7953
7955
  role: cleanPublicText(frontmatter.role || slug.replace(/[-_]/g, ' '), 120),
7954
7956
  description: cleanPublicText(frontmatter.description || '', 240),
7957
+ type: memberTypeOf(frontmatter).type,
7955
7958
  path: memberPath,
7956
7959
  };
7957
7960
  })
@@ -8018,6 +8021,8 @@ function chooseTaskOwner({ purpose, tag, requestedOwner, root = process.cwd() }
8018
8021
  const words = publicWords(`${purpose || ''} ${tag || ''}`);
8019
8022
  let best = null;
8020
8023
  for (const member of members) {
8024
+ // A person can be named as an owner, but routing never hands them work as if they were an AI.
8025
+ if (member.type !== 'ai') continue;
8021
8026
  const score = scoreTeamMember(member, words, tag);
8022
8027
  if (!best || score > best.score) best = { member, score };
8023
8028
  }
@@ -13465,6 +13470,8 @@ async function run(args) {
13465
13470
 
13466
13471
  module.exports = {
13467
13472
  run,
13473
+ chooseTaskOwner,
13474
+ readTeamMembers,
13468
13475
  createTaskApiServer,
13469
13476
  taskDayGroups,
13470
13477
  taskDayTextGroups,