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
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ atris init && atris
|
|
|
24
24
|
|
|
25
25
|
## For Coding Agents
|
|
26
26
|
|
|
27
|
-
**Read [`FOR_AGENTS.md`](FOR_AGENTS.md) first
|
|
27
|
+
**Read [`FOR_AGENTS.md`](FOR_AGENTS.md) first**: why agents adopt Atris, 60-second start, spread-it instructions.
|
|
28
28
|
|
|
29
29
|
If you're an agent, run:
|
|
30
30
|
|
|
@@ -110,6 +110,17 @@ atris site publish dist --slug my-site --spa
|
|
|
110
110
|
atris site publish . --slug my-next-app --profile app --build # Next.js static export
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
### Bring people together with the Host
|
|
114
|
+
|
|
115
|
+
The Host is a team member that gets to know everyone in a room (a company, a club, your own network) through one fun question at a time, keeps a card for each person, and introduces two people only when both say yes. Then it offers to find a time and checks that it happened. It never reads anyone's messages and keeps no scores.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
atris member install host
|
|
119
|
+
atris host setup --room personal # your own network: it asks you, and drafts messages for you to send
|
|
120
|
+
atris host import people.jsonl # bulk add people and who already knows whom
|
|
121
|
+
atris host room # the host's weekly read
|
|
122
|
+
```
|
|
123
|
+
|
|
113
124
|
## Chat With Atris 2
|
|
114
125
|
|
|
115
126
|
`ax` is the Atris 2 chat and coding-agent CLI. It uses the hosted Atris cloud by default, streams text, shows tool activity, and keeps fresh installs away from local setup.
|
|
@@ -123,7 +134,7 @@ ax --max "reason through this hard decision"
|
|
|
123
134
|
ax --doctor
|
|
124
135
|
```
|
|
125
136
|
|
|
126
|
-
Use Fast for quick chat and low-latency Atris loops. Use Pro for deeper tool loops. Use Max for the hardest jobs
|
|
137
|
+
Use Fast for quick chat and low-latency Atris loops. Use Pro for deeper tool loops. Use Max for the hardest jobs; it runs the highest-reasoning model and takes longer per turn. Advanced local workspace mode is opt-in with `--local` plus `AX_BACKEND_URL`.
|
|
127
138
|
|
|
128
139
|
## Play AgentXP
|
|
129
140
|
|
|
@@ -267,9 +278,30 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
|
|
|
267
278
|
|
|
268
279
|
## Engines
|
|
269
280
|
|
|
270
|
-
An engine is the intelligence that builds a mission. Every installed headless coding CLI is a swappable worker behind one contract: a bounded prompt goes in, verified proof comes out, and engines never self-certify their own work. The house default is `atris-fast` (`ax`);
|
|
281
|
+
An engine is the intelligence that builds a mission. Every installed headless coding CLI is a swappable worker behind one contract: a bounded prompt goes in, verified proof comes out, and engines never self-certify their own work. The house default is `atris-fast` (`ax`); `claude`, `codex`, `cursor`, `devin`, `grok`, and others join when their CLIs are installed.
|
|
282
|
+
|
|
283
|
+
### Who does each job
|
|
284
|
+
|
|
285
|
+
`ROSTER.md` says which tool and model does each job. Edit it any time; every project, team member, and new chat follows it.
|
|
286
|
+
|
|
287
|
+
```markdown
|
|
288
|
+
## build
|
|
289
|
+
- claude code, model: opus 5.5
|
|
290
|
+
|
|
291
|
+
## review
|
|
292
|
+
- codex, model: gpt-6-astra, effort: medium, max: 20 min, prep: search
|
|
293
|
+
- claude code, model: opus 5.5
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The first ready worker leads and the rest are backups. `~/.atris/ROSTER.md` covers every project on the machine; `atris/ROSTER.md` overrides it for one project. A worker that stalls sits out 30 minutes and the next one takes over.
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
atris engine roster # who does what, with each worker's recent record
|
|
300
|
+
atris engine assign build claude --model "opus 5.5" # change a job's lead
|
|
301
|
+
atris engine roster --available # tools and models on this machine
|
|
302
|
+
```
|
|
271
303
|
|
|
272
|
-
Show
|
|
304
|
+
Show every installed engine and the current default:
|
|
273
305
|
|
|
274
306
|
```bash
|
|
275
307
|
atris engine
|
|
@@ -290,7 +322,7 @@ atris mission run "fix the flaky login test" --engine codex
|
|
|
290
322
|
|
|
291
323
|
### Fleet Flight
|
|
292
324
|
|
|
293
|
-
`atris mission run --fleet` staffs every idle installed engine on the board's claimable safe-lane tasks
|
|
325
|
+
`atris mission run --fleet` staffs every idle installed engine on the board's claimable safe-lane tasks: one mission per task, one worktree per engine. Builds run in parallel; arrivals land serially with rebase-before-ship, so a rebase conflict pauses that landing (never auto-resolved) and keeps its worktree instead of orphaning work. Each flight writes a receipt to `atris/runs/fleet-<stamp>.json`.
|
|
294
326
|
|
|
295
327
|
```bash
|
|
296
328
|
atris mission run --fleet --slots 3 # staff up to 3 engines
|
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 ...".
|
|
@@ -26,29 +26,29 @@ One contract, eleven live profiles. The orchestrator writes a bounded task promp
|
|
|
26
26
|
|
|
27
27
|
## Pick the job owner first
|
|
28
28
|
|
|
29
|
-
Before choosing an engine for search, build, or
|
|
29
|
+
Before choosing an engine for search, build, review, or any other job, run `atris engine roster` and use that job's lead worker. Only deviate when the user names an engine.
|
|
30
30
|
|
|
31
|
-
The
|
|
31
|
+
**The file.** `~/.atris/ROSTER.md` covers every project on the machine; `atris/ROSTER.md` overrides it job by job. Each `## job` lists workers in order, one per line, tool then model:
|
|
32
32
|
|
|
33
33
|
```
|
|
34
34
|
## review
|
|
35
|
-
- codex, model: gpt-6-astra, effort: medium, max: 20 min
|
|
35
|
+
- codex, model: gpt-6-astra, effort: medium, max: 20 min, prep: search
|
|
36
36
|
- claude code, model: opus 5.5
|
|
37
37
|
|
|
38
38
|
## small build
|
|
39
|
-
- devin, model: swe-2-max, until 2026-10-24
|
|
39
|
+
- devin, model: swe-2-max, max: 20 min, until 2026-10-24
|
|
40
40
|
- grok, model: grok 4.7 fast
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The first worker that is ready and not expired leads; the rest are
|
|
43
|
+
The first worker that is ready and not expired leads; the rest are backups in order, and the whole list is the job's team for parallel parts. Jobs are open-ended: add `## quick fixes (like build)` when the name does not say its kind. Use the small build lead for small, tightly specified slices.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
**Changing it.** Edit the file, or: `atris engine assign <job> <tool> [--model --effort --max --prep search --days N]` sets the lead and keeps the rest; `--add`, `--remove <tool>`, `--promote <tool>` (move an existing worker to the top, line unchanged), `--clear`; `--everywhere` writes the machine file; `--session` (with `ATRIS_ROSTER_SESSION=<name>`) changes this shell only. `atris engine roster --available` lists installed tools and models.
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
**What happens on its own.** A worker that stalls or loses its connection sits out 30 minutes ("cooling") and the next worker takes the job; a failed task (tests failed) is not handed over and does not bench anyone. Every run is recorded; `atris engine roster --runs` lists them and the roster shows each worker's recent record. When the record says the order is wrong, the roster prints one suggestion with the exact command. It never edits the file: run it only when the owner says so.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
**Heavy models.** Astra and Fable should judge, not explore. Give their line `prep: search`: the search lead writes a brief of at most 300 lines first, and if prep fails the worker runs as usual.
|
|
50
50
|
|
|
51
|
-
Team members
|
|
51
|
+
**Team members.** Navigators, researchers, and scouts do search; validators, reviewers, and judges do review; everyone else builds. A `## team` line overrides: `- researcher: search` or `- judge: claude code, model: opus 5.5`. `member run`, owned missions, and autopilot phases use the member's pick; `--engine` still wins. Parked members are skipped; `atris team park <name>` and `atris team unpark <name>` toggle it.
|
|
52
52
|
|
|
53
53
|
## three verbs
|
|
54
54
|
|
package/bin/atris.js
CHANGED
|
@@ -574,6 +574,7 @@ function showHelpAll() {
|
|
|
574
574
|
console.log(' who - Show local engines and team members working, waiting, done, or stale');
|
|
575
575
|
console.log(' stream - Watch the whole team work live in one terminal');
|
|
576
576
|
console.log(' team - One team view: members, roles, engine assignments (presence for live)');
|
|
577
|
+
console.log(' host - The Host: gets to know people, makes two-yes introductions (atris member install host)');
|
|
577
578
|
console.log(' watch - Turn one sentence into an always-on background watcher');
|
|
578
579
|
console.log(' ctop - Show a process-first live agent CPU/memory view');
|
|
579
580
|
console.log(' doctor - Node/task/auth/workspace readiness (--json for agents)');
|
|
@@ -716,6 +717,8 @@ function showHelpAll() {
|
|
|
716
717
|
console.log(' usage - Show developer API usage');
|
|
717
718
|
console.log(' api-key - Create, list, rotate, or revoke a developer API key');
|
|
718
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');
|
|
719
722
|
console.log(' design - Extract a site design system, check brand adherence, search brands');
|
|
720
723
|
console.log(' mcp - Run the atris MCP server (stdio) for Claude Desktop and Cursor');
|
|
721
724
|
console.log('');
|
|
@@ -2367,6 +2370,15 @@ if (command === 'guide') {
|
|
|
2367
2370
|
Promise.resolve(require('../commands/topup').topupCommand(process.argv.slice(3)))
|
|
2368
2371
|
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2369
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); });
|
|
2370
2382
|
} else if (command === '_resolve') {
|
|
2371
2383
|
// Hidden: resolve a profile name query → print exact profile name
|
|
2372
2384
|
require('../commands/auth').resolveProfile();
|
package/commands/autoland.js
CHANGED
|
@@ -130,13 +130,38 @@ function flag(args, name, fallback = '') {
|
|
|
130
130
|
return args[idx + 1] || fallback;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
function runOwnCli(root, cliArgs) {
|
|
133
|
+
function runOwnCli(root, cliArgs, timeoutMs = 300000) {
|
|
134
134
|
// Always spawn the bin this module shipped with. Resolving through the
|
|
135
135
|
// target root or a global `atris` breaks whenever the tick runs a project
|
|
136
136
|
// that isn't the CLI repo on a machine without a global install (CI, cron).
|
|
137
137
|
const bin = path.resolve(__dirname, '..', 'bin', 'atris.js');
|
|
138
|
-
const result = spawnSync(process.execPath, [bin, ...cliArgs], { cwd: root, encoding: 'utf8', timeout:
|
|
139
|
-
return {
|
|
138
|
+
const result = spawnSync(process.execPath, [bin, ...cliArgs], { cwd: root, encoding: 'utf8', timeout: timeoutMs });
|
|
139
|
+
return {
|
|
140
|
+
status: result.status,
|
|
141
|
+
stdout: String(result.stdout || ''),
|
|
142
|
+
stderr: String(result.stderr || ''),
|
|
143
|
+
error_code: result.error && result.error.code ? result.error.code : null,
|
|
144
|
+
signal: result.signal || null,
|
|
145
|
+
timeout_ms: timeoutMs,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The accept sweep walks every pending-review row under --all, so a fixed
|
|
150
|
+
// 300s child timeout kills a real backlog mid-accept. Size the window from
|
|
151
|
+
// the queue: roughly 8s a row on top of a 60s floor, never under the old
|
|
152
|
+
// 300s default, capped at 25min.
|
|
153
|
+
function acceptSweepTimeoutMs(pendingRows) {
|
|
154
|
+
return Math.min(1500000, Math.max(300000, 60000 + pendingRows * 8000));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// A killed child truncates stdout and often leaves stderr empty, so the
|
|
158
|
+
// spawn failure itself is the first thing to name. Without this a timed-out
|
|
159
|
+
// sweep reports "output unreadable" forever.
|
|
160
|
+
function describeAcceptKillCause(accept) {
|
|
161
|
+
if (accept.error_code === 'ETIMEDOUT') return `auto-accept timed out at ${accept.timeout_ms}ms`;
|
|
162
|
+
if (accept.error_code) return `auto-accept spawn failed: ${accept.error_code}`;
|
|
163
|
+
if (accept.signal) return `auto-accept killed by ${accept.signal}`;
|
|
164
|
+
return null;
|
|
140
165
|
}
|
|
141
166
|
|
|
142
167
|
function readProjection(root) {
|
|
@@ -759,7 +784,11 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
759
784
|
const cliArgs = ['task', 'auto-accept-certified', '--json', '--certify-first'];
|
|
760
785
|
if (policy.accept_all) cliArgs.push('--all');
|
|
761
786
|
else if (policy.strict_verify === false) cliArgs.push('--no-strict-verify');
|
|
762
|
-
const
|
|
787
|
+
const pendingReviewRows = readProjection(root)
|
|
788
|
+
.filter((task) => task && task.status === 'review')
|
|
789
|
+
.filter((task) => String(task.review?.approval_status || task.metadata?.approval_status || 'pending') === 'pending')
|
|
790
|
+
.length;
|
|
791
|
+
const accept = runOwnCli(root, cliArgs, acceptSweepTimeoutMs(pendingReviewRows));
|
|
763
792
|
try {
|
|
764
793
|
const parsed = JSON.parse(accept.stdout);
|
|
765
794
|
const results = Array.isArray(parsed.results) ? parsed.results : [];
|
|
@@ -801,7 +830,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
801
830
|
receipt.skipped = parsed.skipped ?? null;
|
|
802
831
|
receipt.undercounted = Boolean(parsed.undercounted);
|
|
803
832
|
} catch (err) {
|
|
804
|
-
receipt.accept_error = accept.stderr.slice(0, 200) || 'auto-accept output unreadable';
|
|
833
|
+
receipt.accept_error = describeAcceptKillCause(accept) || accept.stderr.slice(0, 200) || 'auto-accept output unreadable';
|
|
805
834
|
}
|
|
806
835
|
|
|
807
836
|
// 2b. tell the operator the moment something lands: one text, one landing
|
|
@@ -1108,6 +1137,8 @@ function autolandCommand(args = []) {
|
|
|
1108
1137
|
module.exports = {
|
|
1109
1138
|
autolandCommand,
|
|
1110
1139
|
digestNextMoves,
|
|
1140
|
+
acceptSweepTimeoutMs,
|
|
1141
|
+
describeAcceptKillCause,
|
|
1111
1142
|
digestStoryRows,
|
|
1112
1143
|
digestTickStatus,
|
|
1113
1144
|
engineAnswerValidationLine,
|
package/commands/autopilot.js
CHANGED
|
@@ -31,6 +31,10 @@ const { ownerIdentity } = require('../utils/owner-identity');
|
|
|
31
31
|
const pkg = require('../package.json');
|
|
32
32
|
|
|
33
33
|
const PHASE_TIMEOUT = 600000; // 10 min per phase
|
|
34
|
+
// A phase's cap covers its prep pass too: prep's elapsed time comes out of
|
|
35
|
+
// the cap before the phase command starts. The floor keeps a slow prep from
|
|
36
|
+
// leaving the phase no room at all.
|
|
37
|
+
const PHASE_MIN_AFTER_PREP_MS = 30000;
|
|
34
38
|
|
|
35
39
|
function looksOwnerClaimed(claimed) {
|
|
36
40
|
const text = String(claimed || '').trim().toLowerCase();
|
|
@@ -521,7 +525,17 @@ function buildPhaseRunner(phase, promptFile, cwd = process.cwd(), allowedTools =
|
|
|
521
525
|
...(picked.engine.roster_effort ? { effort: picked.engine.roster_effort } : {}),
|
|
522
526
|
});
|
|
523
527
|
const maxSeconds = Number(picked.engine.roster_max_seconds) || 0;
|
|
524
|
-
return {
|
|
528
|
+
return {
|
|
529
|
+
command,
|
|
530
|
+
timeoutMs: maxSeconds > 0 ? maxSeconds * 1000 : null,
|
|
531
|
+
engineId: picked.engine.id,
|
|
532
|
+
maxSeconds,
|
|
533
|
+
model: picked.model || '',
|
|
534
|
+
effort: picked.engine.roster_effort || '',
|
|
535
|
+
job: picked.job || '',
|
|
536
|
+
member: AUTOPILOT_PHASE_MEMBERS[phase],
|
|
537
|
+
prep: picked.engine.roster_prep || '',
|
|
538
|
+
};
|
|
525
539
|
} finally {
|
|
526
540
|
if (previous === undefined) delete process.env.ATRIS_RUNNER_PROFILE;
|
|
527
541
|
else process.env.ATRIS_RUNNER_PROFILE = previous;
|
|
@@ -532,6 +546,54 @@ function buildPhaseRunnerCommand(phase, promptFile, cwd = process.cwd()) {
|
|
|
532
546
|
return buildPhaseRunner(phase, promptFile, cwd).command;
|
|
533
547
|
}
|
|
534
548
|
|
|
549
|
+
// One run-record line for a phase a roster worker ran. Never throws.
|
|
550
|
+
function recordPhaseRosterRun(root, phase, runner, startedMs, outcome, err = null) {
|
|
551
|
+
if (!runner || !runner.engineId) return;
|
|
552
|
+
try {
|
|
553
|
+
const { appendRosterRun, parseRunUsage } = require('../lib/roster-runs');
|
|
554
|
+
const { engineRunsView } = require('../lib/roster-models');
|
|
555
|
+
const { prepRecordFields } = require('../lib/roster-prep');
|
|
556
|
+
appendRosterRun(root, {
|
|
557
|
+
at: new Date(startedMs).toISOString(),
|
|
558
|
+
job: runner.job || '',
|
|
559
|
+
member: runner.member || '',
|
|
560
|
+
engine: runner.engineId,
|
|
561
|
+
model: runner.model || engineRunsView(runner.engineId).model || '',
|
|
562
|
+
effort: runner.effort || '',
|
|
563
|
+
max_seconds: runner.maxSeconds || 0,
|
|
564
|
+
seconds: Math.max(0, (Date.now() - startedMs) / 1000),
|
|
565
|
+
task: `autopilot ${phase}`,
|
|
566
|
+
source: 'autopilot',
|
|
567
|
+
...prepRecordFields(runner.prepResult),
|
|
568
|
+
...parseRunUsage({ stdout: outcome.stdout || (err && err.stdout) || '', stderr: (err && err.stderr) || '' }),
|
|
569
|
+
outcome: outcome.outcome,
|
|
570
|
+
...(outcome.detail ? { detail: outcome.detail } : {}),
|
|
571
|
+
});
|
|
572
|
+
} catch { /* best effort */ }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// How a phase ended when its command threw: a timeout stalled, a signal or
|
|
576
|
+
// nonzero exit failed, a credit wall on stderr is credit out.
|
|
577
|
+
function phaseFailureOutcome(err, runner) {
|
|
578
|
+
if (isPhaseTimeoutError(err)) {
|
|
579
|
+
const cap = Number(runner && runner.maxSeconds) || 0;
|
|
580
|
+
return { outcome: 'stalled', detail: cap >= 60 ? `stalled at ${Math.round(cap / 60)} min` : cap > 0 ? `stalled at ${cap}s` : 'hit its time limit' };
|
|
581
|
+
}
|
|
582
|
+
if (isPhaseKillError(err)) return { outcome: 'failed', detail: `stopped by ${err.signal || 'a signal'}` };
|
|
583
|
+
const { engineFailureHealthStatus } = require('../lib/engine-registry');
|
|
584
|
+
const stderr = String(err && err.stderr || '');
|
|
585
|
+
if (engineFailureHealthStatus({ status: 'errored', stderr }) === 'credit_out') return { outcome: 'credit out', detail: 'the engine hit its usage limit' };
|
|
586
|
+
const line = stderr.trim().split('\n').pop() || String(err && err.message || '').split('\n')[0];
|
|
587
|
+
return { outcome: 'failed', detail: line || `exited ${err && err.status}` };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// What the prep worker reads as the task: the tick's task title.
|
|
591
|
+
function autopilotPhaseTaskTitle(context, phase) {
|
|
592
|
+
const task = context && context.task;
|
|
593
|
+
if (task && typeof task === 'object') return String(task.title || task.display_id || `autopilot ${phase}`);
|
|
594
|
+
return String(task || `autopilot ${phase}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
535
597
|
/**
|
|
536
598
|
* Run a phase via the configured runner subprocess.
|
|
537
599
|
*/
|
|
@@ -539,20 +601,43 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
539
601
|
const { verbose = false } = options;
|
|
540
602
|
let timeout = options.timeout || PHASE_TIMEOUT;
|
|
541
603
|
|
|
542
|
-
|
|
604
|
+
let prompt = buildPrompt(phase, context, options);
|
|
543
605
|
const tmpFile = path.join(process.cwd(), '.autopilot-prompt.tmp');
|
|
544
606
|
fs.writeFileSync(tmpFile, prompt);
|
|
545
607
|
|
|
608
|
+
let rosterRunner = null;
|
|
609
|
+
let startedMs = Date.now();
|
|
546
610
|
try {
|
|
547
611
|
let cmd = options.cmdOverride;
|
|
548
612
|
if (!cmd) {
|
|
549
613
|
const runner = buildPhaseRunner(phase, tmpFile);
|
|
550
|
-
|
|
551
|
-
// A roster time cap stops the phase at that time
|
|
614
|
+
if (runner.engineId) rosterRunner = runner;
|
|
615
|
+
// A roster time cap stops the phase at that time, and prep runs inside
|
|
616
|
+
// it: the phase gets what remains after the pass, floored so it can
|
|
617
|
+
// still try.
|
|
552
618
|
if (runner.timeoutMs) timeout = runner.timeoutMs;
|
|
619
|
+
// A worker whose roster line asks for prep gets its brief first, the
|
|
620
|
+
// same pass fleet runs; the launch reads the prompt file, brief added.
|
|
621
|
+
if (runner.engineId && runner.prep) {
|
|
622
|
+
const prepStartedMs = Date.now();
|
|
623
|
+
const { runPrepPassSync, withPrepBrief } = require('../lib/roster-prep');
|
|
624
|
+
runner.prepResult = runPrepPassSync({
|
|
625
|
+
prepJob: runner.prep,
|
|
626
|
+
forJob: runner.job || AUTOPILOT_PHASE_MEMBERS[phase],
|
|
627
|
+
task: { display_id: `autopilot ${phase}`, title: autopilotPhaseTaskTitle(context, phase) },
|
|
628
|
+
prompt,
|
|
629
|
+
root: process.cwd(),
|
|
630
|
+
record: { member: runner.member || '' },
|
|
631
|
+
});
|
|
632
|
+
prompt = withPrepBrief(prompt, runner.prepResult);
|
|
633
|
+
fs.writeFileSync(tmpFile, prompt);
|
|
634
|
+
timeout = Math.max(PHASE_MIN_AFTER_PREP_MS, timeout - (Date.now() - prepStartedMs));
|
|
635
|
+
}
|
|
636
|
+
cmd = runner.command;
|
|
553
637
|
}
|
|
554
638
|
const env = { ...process.env };
|
|
555
639
|
delete env.CLAUDECODE;
|
|
640
|
+
startedMs = Date.now();
|
|
556
641
|
const output = execPhaseCommandSync(cmd, {
|
|
557
642
|
cwd: process.cwd(),
|
|
558
643
|
encoding: 'utf8',
|
|
@@ -563,10 +648,19 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
563
648
|
});
|
|
564
649
|
|
|
565
650
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
651
|
+
recordPhaseRosterRun(process.cwd(), phase, rosterRunner, startedMs, { outcome: 'landed', stdout: output || '' });
|
|
566
652
|
return { prompt, output: output || '' };
|
|
567
653
|
} catch (err) {
|
|
568
654
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
655
|
+
recordPhaseRosterRun(process.cwd(), phase, rosterRunner, startedMs, phaseFailureOutcome(err, rosterRunner), err);
|
|
569
656
|
if (isPhaseTimeoutError(err)) {
|
|
657
|
+
// A roster worker that stalls sits out the cooldown, so the next
|
|
658
|
+
// phase run goes to the job's backup.
|
|
659
|
+
if (rosterRunner) {
|
|
660
|
+
try {
|
|
661
|
+
require('../lib/engine-registry').recordEngineRunHealth(rosterRunner.engineId, { timed_out: true, max_seconds: rosterRunner.maxSeconds }, process.cwd());
|
|
662
|
+
} catch { /* best effort */ }
|
|
663
|
+
}
|
|
570
664
|
throw new Error(`${phase} phase timed out after ${timeout / 1000}s (configured runner hit the wall; any work it committed survives, reconcile from pre-tick HEADs)`);
|
|
571
665
|
}
|
|
572
666
|
if (isPhaseKillError(err)) {
|
|
@@ -1419,7 +1513,7 @@ function parseProposedBlock(lines) {
|
|
|
1419
1513
|
// behind it, the configured runner runs exactly as before.
|
|
1420
1514
|
function buildPlanReviewRunner(promptFile, cwd = process.cwd(), timeout = 180000) {
|
|
1421
1515
|
const runner = buildPhaseRunner('review', promptFile, cwd, 'Bash,Read,Grep,Glob');
|
|
1422
|
-
return { command: runner.command, timeoutMs: runner.timeoutMs || timeout };
|
|
1516
|
+
return { ...runner, command: runner.command, timeoutMs: runner.timeoutMs || timeout };
|
|
1423
1517
|
}
|
|
1424
1518
|
|
|
1425
1519
|
/**
|
|
@@ -1429,12 +1523,15 @@ function buildPlanReviewRunner(promptFile, cwd = process.cwd(), timeout = 180000
|
|
|
1429
1523
|
function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
1430
1524
|
const tmpFile = path.join(cwd, '.autopilot-plan-review.tmp');
|
|
1431
1525
|
fs.writeFileSync(tmpFile, prompt);
|
|
1526
|
+
let runner = null;
|
|
1527
|
+
let startedMs = Date.now();
|
|
1432
1528
|
try {
|
|
1433
|
-
|
|
1529
|
+
runner = buildPlanReviewRunner(tmpFile, cwd, timeout);
|
|
1434
1530
|
const cmd = runner.command;
|
|
1435
1531
|
timeout = runner.timeoutMs;
|
|
1436
1532
|
const env = { ...process.env };
|
|
1437
1533
|
delete env.CLAUDECODE;
|
|
1534
|
+
startedMs = Date.now();
|
|
1438
1535
|
const output = execPhaseCommandSync(cmd, {
|
|
1439
1536
|
cwd,
|
|
1440
1537
|
encoding: 'utf8',
|
|
@@ -1443,8 +1540,10 @@ function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
|
1443
1540
|
maxBuffer: 10 * 1024 * 1024,
|
|
1444
1541
|
env,
|
|
1445
1542
|
});
|
|
1543
|
+
recordPhaseRosterRun(cwd, 'plan review', runner, startedMs, { outcome: 'landed', stdout: output || '' });
|
|
1446
1544
|
return output || '';
|
|
1447
1545
|
} catch (err) {
|
|
1546
|
+
recordPhaseRosterRun(cwd, 'plan review', runner, startedMs, phaseFailureOutcome(err, runner), err);
|
|
1448
1547
|
if (err.stdout) return err.stdout;
|
|
1449
1548
|
throw err;
|
|
1450
1549
|
} finally {
|