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.
- package/atris/features/README.md +8 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/commerce/SKILL.md +96 -0
- package/bin/atris.js +11 -0
- package/commands/buy.js +242 -0
- package/commands/member.js +35 -6
- package/commands/mission.js +4 -1
- package/commands/task.js +7 -0
- package/commands/transactions.js +115 -0
- package/commands/youtube.js +22 -0
- package/lib/commerce.js +75 -0
- package/lib/engine-registry.js +17 -0
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +15 -1
- package/lib/member-scaffold.js +2 -0
- package/lib/member-type.js +27 -0
- package/lib/runner-command.js +37 -2
- package/package.json +1 -1
- package/scripts/det/ytnotes +73 -13
- package/scripts/det/ytrail-bench.js +214 -0
- package/scripts/det/ytrail-eval.js +128 -56
package/atris/features/README.md
CHANGED
|
@@ -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/*
|
package/atris/skills/README.md
CHANGED
|
@@ -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();
|
package/commands/buy.js
ADDED
|
@@ -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 };
|
package/commands/member.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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', '');
|
package/commands/mission.js
CHANGED
|
@@ -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,
|