atris 3.68.0 → 3.68.2
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/skills/youtube/SKILL.md +12 -1
- package/bin/atris.js +40 -1
- package/commands/done.js +374 -0
- package/commands/member.js +24 -1
- package/commands/workflow.js +1 -1
- package/commands/youtube.js +12 -2
- package/lib/falsifier-probe.js +6 -0
- package/lib/first-minute.js +257 -6
- package/lib/known-commands.js +1 -1
- package/package.json +5 -2
- package/scripts/det/ytnotes +11 -0
- package/utils/backend-root.js +4 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: youtube
|
|
3
3
|
description: "YouTube discovery and learning. Get watch permalinks with atris youtube search QUERY (free, local ytsearch/yt-dlp). On 429 use printed rows if any; else the CLI retries once, then cached rows if printed, else STOP. Never run --paid after a 429. --paid only when the user explicitly asked to buy permalinks (5 credits). After a URL is picked, atris youtube notes URL (free, ephemeral unless --save). atris youtube process only to store knowledge (5 credits). Never paste tokens. Never /auth/cli. Mint with atris login --agent from a stored login. Never summarize a video from model memory. Triggers on: youtube search, find videos, paid youtube search, any youtube.com or youtu.be link, youtube, video, watch this, notes on this."
|
|
4
|
-
version: 2.18.
|
|
4
|
+
version: 2.18.24
|
|
5
5
|
tags:
|
|
6
6
|
- youtube
|
|
7
7
|
- research
|
|
@@ -11,6 +11,17 @@ tags:
|
|
|
11
11
|
|
|
12
12
|
# YouTube Skill
|
|
13
13
|
|
|
14
|
+
## Route first: learning vs product
|
|
15
|
+
|
|
16
|
+
Two rails. Pick one before running anything.
|
|
17
|
+
|
|
18
|
+
- **Learning rail (free):** `atris youtube notes URL` or `atris youtube teach URL`. Local yt-dlp captions plus a local AI writer, zero credits, no login. Use it when the ask is "learn from", "notes on", "alpha", "rabbit hole", "what does this say". This replaces the old alpha-learn skill.
|
|
19
|
+
- **Product rail (5 credits):** `atris youtube process URL` (bare `atris youtube URL` is the same call). Stores the analysis in the Atris backend, needs a stored login, and is gated on a filled Apply. Use it only when the ask is "process", "store", "add to knowledge", or `--agent ID --store`.
|
|
20
|
+
|
|
21
|
+
Process is gated. It exits 2 with `write one apply (change + receipt) before process.` until `atris/wiki/briefs/youtube-<id>.apply.md` (relative to the current directory) has real `change:` and `receipt:` lines. `fill this` does not count, and a notes/search keep-sidecar (`change: apply atris/experiments/...`) does not count. The CLI prints the exact file on the next line. It writes a fill-this stub there only when `atris/wiki/` already exists; otherwise create the file yourself. The gate runs before any login or billing, so a refusal never spends credits. Write the Apply from what the person will actually change, not a placeholder.
|
|
22
|
+
|
|
23
|
+
Notes writer: Gemini Flash through the Antigravity CLI (`agy -p`) when installed, else Haiku (`claude`), then Codex, then Cursor. The Antigravity editor's own `agy` launcher has no `-p` mode and is skipped. `notes by <writer>` names who wrote them. `check: fill this` after notes, teach, or search means the video had no number or named mechanism to build a check from; it is not an error.
|
|
24
|
+
|
|
14
25
|
## Feature map
|
|
15
26
|
|
|
16
27
|
Hard path. Follow in order. Soft memory does not override this.
|
package/bin/atris.js
CHANGED
|
@@ -6,6 +6,17 @@ process.on('unhandledRejection', (err) => {
|
|
|
6
6
|
process.exit(1);
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
+
// node:sqlite (task state) ships unflagged in Node 22.13. On older Node the
|
|
10
|
+
// first require crashes with ERR_UNKNOWN_BUILTIN_MODULE before any help prints.
|
|
11
|
+
{
|
|
12
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
13
|
+
if (major < 22 || (major === 22 && minor < 13)) {
|
|
14
|
+
console.error(`atris needs Node 22.13 or newer. this is Node ${process.versions.node}.`);
|
|
15
|
+
console.error('upgrade from https://nodejs.org, then run atris again.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
const fs = require('fs');
|
|
10
21
|
const path = require('path');
|
|
11
22
|
const { exec, spawnSync } = require('child_process');
|
|
@@ -67,9 +78,14 @@ const {
|
|
|
67
78
|
isBareAtrisFlag,
|
|
68
79
|
isFirstTalkLine,
|
|
69
80
|
isLeftoverVerbLook,
|
|
81
|
+
isWhatsNextLine,
|
|
70
82
|
listUserVisibleWork,
|
|
83
|
+
loadLocalTasks,
|
|
71
84
|
personName,
|
|
72
85
|
renderFirstTalk,
|
|
86
|
+
renderWhatsNextAsk,
|
|
87
|
+
firstTalkBuilder,
|
|
88
|
+
shippedHumanStarter,
|
|
73
89
|
shouldAutoInitFresh,
|
|
74
90
|
} = require('../lib/first-minute');
|
|
75
91
|
|
|
@@ -501,6 +517,7 @@ function showHelpShort() {
|
|
|
501
517
|
console.log(' atris later "..." remember without a room');
|
|
502
518
|
console.log(' atris guide "..." translate your words');
|
|
503
519
|
console.log(' atris do work already here');
|
|
520
|
+
console.log(' atris done land what your agent built');
|
|
504
521
|
console.log(' atris spaceship Keep working for a few hours');
|
|
505
522
|
console.log(' atris autopilot Keep working until you stop');
|
|
506
523
|
console.log(' atris mission the one goal');
|
|
@@ -512,6 +529,7 @@ function helpCatalog() {
|
|
|
512
529
|
return [
|
|
513
530
|
{ name: 'init', summary: 'scaffold atris/ in this project', json: false, interactive_risk: 'medium' },
|
|
514
531
|
{ name: 'guide', summary: 'translate plain english into the right atris move', json: true, interactive_risk: 'none' },
|
|
532
|
+
{ name: 'done', summary: 'land what your coding agent built: check, accept, sign', json: true, interactive_risk: 'low' },
|
|
515
533
|
{ name: 'task', summary: 'local task plane: new, claim, ready, accept', json: true, interactive_risk: 'low' },
|
|
516
534
|
{ name: 'review', summary: 'validate work and capture learnings', json: false, interactive_risk: 'low' },
|
|
517
535
|
{ name: 'recap', summary: 'spoken recap of what your team did', json: true, interactive_risk: 'low' },
|
|
@@ -532,6 +550,7 @@ function showHelpAll() {
|
|
|
532
550
|
console.log(' atris later "..." remember without a room');
|
|
533
551
|
console.log(' atris guide "..." translate your words');
|
|
534
552
|
console.log(' atris do work already here');
|
|
553
|
+
console.log(' atris done land what your agent built');
|
|
535
554
|
console.log(' atris spaceship Keep working for a few hours');
|
|
536
555
|
console.log(' atris autopilot Keep working until you stop');
|
|
537
556
|
console.log(' atris mission the one goal');
|
|
@@ -1430,7 +1449,7 @@ async function interactiveEntry(userInput, options = {}) {
|
|
|
1430
1449
|
return 0;
|
|
1431
1450
|
}
|
|
1432
1451
|
console.log('');
|
|
1433
|
-
console.log(renderFirstTalk({ person: who, folder: room, starter }));
|
|
1452
|
+
console.log(renderFirstTalk({ person: who, folder: room, starter, builder: firstTalkBuilder() }));
|
|
1434
1453
|
return 0;
|
|
1435
1454
|
}
|
|
1436
1455
|
|
|
@@ -1519,6 +1538,23 @@ async function interactiveEntry(userInput, options = {}) {
|
|
|
1519
1538
|
// Placeholder or missing MAP is already a first-minute job. Do not send the
|
|
1520
1539
|
// operator to a rewrite lobby, and do not treat `test` as a new first task.
|
|
1521
1540
|
if (mapStatus !== 'ready') {
|
|
1541
|
+
// A person who already shipped their first want here can say the next one.
|
|
1542
|
+
// It becomes their new task, the same way the first one did.
|
|
1543
|
+
const nextWant = String(userInput || '').trim();
|
|
1544
|
+
const shipped = nextWant && !options.asJson ? shippedHumanStarter(loadLocalTasks(workspaceDir)) : null;
|
|
1545
|
+
if (shipped && !isLeftoverVerbLook(nextWant) && !isFlagLikeAnswer(nextWant) && !isAtrisMetaQuestion(nextWant)) {
|
|
1546
|
+
const who = personName();
|
|
1547
|
+
const room = folderName(workspaceDir);
|
|
1548
|
+
console.log('');
|
|
1549
|
+
if (isWhatsNextLine(nextWant) || isFirstTalkLine(nextWant)) {
|
|
1550
|
+
console.log(renderWhatsNextAsk({ person: who, task: shipped, folder: room }));
|
|
1551
|
+
return 0;
|
|
1552
|
+
}
|
|
1553
|
+
saveContextProfile(workspaceDir, nextWant, { source: 'next_talk' });
|
|
1554
|
+
const starter = createStarterTask(workspaceDir, nextWant);
|
|
1555
|
+
console.log(renderFirstTalk({ person: who, folder: room, starter, builder: firstTalkBuilder() }));
|
|
1556
|
+
return 0;
|
|
1557
|
+
}
|
|
1522
1558
|
const screenOptions = {
|
|
1523
1559
|
root: workspaceDir,
|
|
1524
1560
|
context,
|
|
@@ -2229,6 +2265,9 @@ if (command === 'guide') {
|
|
|
2229
2265
|
} else if (command === 'later') {
|
|
2230
2266
|
const laterCode = require('../commands/later').laterAtris(process.argv.slice(3));
|
|
2231
2267
|
process.exit(Number.isInteger(laterCode) ? laterCode : 0);
|
|
2268
|
+
} else if (command === 'done') {
|
|
2269
|
+
const doneCode = require('../commands/done').doneAtris(process.argv.slice(3));
|
|
2270
|
+
process.exit(Number.isInteger(doneCode) ? doneCode : 0);
|
|
2232
2271
|
} else if (command === 'activate') {
|
|
2233
2272
|
const args = process.argv.slice(3);
|
|
2234
2273
|
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
package/commands/done.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `atris done`: the one command a person runs after their coding agent built
|
|
4
|
+
// the thing. It names what was built, lands it through the real task path
|
|
5
|
+
// (`task ready --verify` then `task accept`, same gates agents face), and ends
|
|
6
|
+
// with a short credits roll and the file to open.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
const { argsWantHelp } = require('../lib/noninteractive');
|
|
12
|
+
const {
|
|
13
|
+
buildFirstMinute,
|
|
14
|
+
firstTalkBuildCommand,
|
|
15
|
+
firstTalkBuilder,
|
|
16
|
+
folderName,
|
|
17
|
+
humanStarterTask,
|
|
18
|
+
isFreshWorkspace,
|
|
19
|
+
listBuiltSince,
|
|
20
|
+
loadLocalTasks,
|
|
21
|
+
mainBuiltFile,
|
|
22
|
+
orderBuilt,
|
|
23
|
+
personName,
|
|
24
|
+
speakFirstMinute,
|
|
25
|
+
spokenBuiltList,
|
|
26
|
+
taskStartedAt,
|
|
27
|
+
} = require('../lib/first-minute');
|
|
28
|
+
|
|
29
|
+
const ATRIS_BIN = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
30
|
+
const DONE_SCHEMA = 'atris.one_lap.v1';
|
|
31
|
+
|
|
32
|
+
// What a page has, in plain words, for the first tag found.
|
|
33
|
+
const HTML_MARKS = [
|
|
34
|
+
['<form', 'the form'],
|
|
35
|
+
['<h1', 'the heading'],
|
|
36
|
+
['<title', 'the page title'],
|
|
37
|
+
['<main', 'the main section'],
|
|
38
|
+
['<body', 'the page body'],
|
|
39
|
+
['<html', 'the page'],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function doneUsage() {
|
|
43
|
+
return 'Usage: atris done [<task>] [--json]';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function shQuote(value) {
|
|
47
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function spokenTitle(task) {
|
|
51
|
+
return String((task && task.title) || 'this').replace(/\s+/g, ' ').trim().replace(/[.,;:!?]+$/g, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readHead(root, file) {
|
|
55
|
+
try {
|
|
56
|
+
const buf = fs.readFileSync(path.join(root, file));
|
|
57
|
+
return buf.subarray(0, 65536);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A check that fails when the built file is gone or no longer holds what was
|
|
64
|
+
// built, so it clears the weak-verifier and empty-folder gates honestly.
|
|
65
|
+
function verifierFor(root, files) {
|
|
66
|
+
const ordered = orderBuilt(files);
|
|
67
|
+
for (const file of ordered) {
|
|
68
|
+
const head = readHead(root, file);
|
|
69
|
+
if (!head || !head.length || head.includes(0)) continue;
|
|
70
|
+
const text = head.toString('utf8');
|
|
71
|
+
if (/\.html?$/i.test(file)) {
|
|
72
|
+
const lower = text.toLowerCase();
|
|
73
|
+
const mark = HTML_MARKS.find(([tag]) => lower.includes(tag));
|
|
74
|
+
if (mark) {
|
|
75
|
+
return {
|
|
76
|
+
command: `grep -qi ${shQuote(mark[0])} ${shQuote(file)}`,
|
|
77
|
+
plain: `${file} has ${mark[1]}`,
|
|
78
|
+
file,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const word = (text.match(/[A-Za-z][A-Za-z0-9_-]{3,}/) || [])[0];
|
|
83
|
+
if (word) {
|
|
84
|
+
return {
|
|
85
|
+
command: `grep -qF ${shQuote(word)} ${shQuote(file)}`,
|
|
86
|
+
plain: `${file} says "${word}"`,
|
|
87
|
+
file,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const file = ordered.find((name) => {
|
|
92
|
+
const head = readHead(root, name);
|
|
93
|
+
return head && head.length;
|
|
94
|
+
});
|
|
95
|
+
if (!file) return null;
|
|
96
|
+
return {
|
|
97
|
+
command: `grep -qa . ${shQuote(file)}`,
|
|
98
|
+
plain: `${file} is there and not empty`,
|
|
99
|
+
file,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// One sentence the ready gate accepts: what the person can do now and why.
|
|
104
|
+
function resultSentence(task) {
|
|
105
|
+
const { explainResult } = require('../lib/autoland');
|
|
106
|
+
const title = spokenTitle(task).replace(/[.!?]/g, '').slice(0, 100).trim();
|
|
107
|
+
const candidates = [
|
|
108
|
+
`You can now open ${title} and use it yourself, so it is real instead of only a plan.`,
|
|
109
|
+
'You can now open what you asked for here and use it yourself, so it is real instead of only a plan.',
|
|
110
|
+
];
|
|
111
|
+
return candidates.find((line) => explainResult(line, { allowCommandMention: true }).ok) || candidates[1];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function openCommand(file) {
|
|
115
|
+
const target = /^[\w./-]+$/.test(file) ? file : shQuote(file);
|
|
116
|
+
if (process.platform === 'darwin') return `open ${target}`;
|
|
117
|
+
if (process.platform === 'win32') return `start ${target}`;
|
|
118
|
+
return `xdg-open ${target}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildCommandFor(task, builder) {
|
|
122
|
+
const pick = builder !== undefined ? builder : firstTalkBuilder();
|
|
123
|
+
// Test runners skip the machine probe; name the default builder so the line stays runnable.
|
|
124
|
+
return firstTalkBuildCommand(pick === undefined ? 'claude' : pick, spokenTitle(task));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function taskRefOf(task) {
|
|
128
|
+
return String((task && (task.display_id || task.legacy_ref || task.id)) || '').trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findTask(tasks, ref) {
|
|
132
|
+
const want = String(ref || '').trim().toLowerCase();
|
|
133
|
+
if (!want) return null;
|
|
134
|
+
return tasks.find((task) => [task.display_id, task.legacy_ref, task.id]
|
|
135
|
+
.some((value) => value && String(value).toLowerCase() === want))
|
|
136
|
+
|| tasks.find((task) => task.id && String(task.id).toLowerCase().startsWith(want))
|
|
137
|
+
|| null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function pickTask(tasks, person) {
|
|
141
|
+
const starter = humanStarterTask(tasks, person);
|
|
142
|
+
if (starter) return starter;
|
|
143
|
+
const who = String(person || '').trim();
|
|
144
|
+
const mine = tasks
|
|
145
|
+
.filter((task) => (task.status === 'claimed' || task.status === 'review')
|
|
146
|
+
&& (!who || String(task.claimed_by || '') === who))
|
|
147
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
|
148
|
+
return mine[0] || null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function runAtris(root, args) {
|
|
152
|
+
const result = spawnSync(process.execPath, [ATRIS_BIN, ...args], {
|
|
153
|
+
cwd: root,
|
|
154
|
+
env: process.env,
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
timeout: 240000,
|
|
157
|
+
});
|
|
158
|
+
let json = null;
|
|
159
|
+
try {
|
|
160
|
+
json = JSON.parse(String(result.stdout || '').trim());
|
|
161
|
+
} catch {
|
|
162
|
+
json = null;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ok: result.status === 0,
|
|
166
|
+
status: result.status,
|
|
167
|
+
stdout: String(result.stdout || ''),
|
|
168
|
+
stderr: String(result.stderr || ''),
|
|
169
|
+
json,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function failureLine(step) {
|
|
174
|
+
const text = `${step.stderr}\n${step.stdout}`;
|
|
175
|
+
if (step.json && (step.json.detail || step.json.reason)) {
|
|
176
|
+
return String(step.json.detail || step.json.reason).split('\n')[0];
|
|
177
|
+
}
|
|
178
|
+
const line = text.split('\n').map((s) => s.trim()).find(Boolean);
|
|
179
|
+
return line || `exit ${step.status}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function liveTask(root, id) {
|
|
183
|
+
try {
|
|
184
|
+
const taskDb = require('../lib/task-db');
|
|
185
|
+
const db = taskDb.open();
|
|
186
|
+
return taskDb.getTask(db, id);
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function receiptFromProof(proof) {
|
|
193
|
+
const match = String(proof || '').match(/Receipt:\s*(\S+\.json)/);
|
|
194
|
+
return match ? match[1] : '';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The receipt is the record. Say on it that the person who owned this work
|
|
198
|
+
// accepted it themselves, through `atris done`.
|
|
199
|
+
function stampOwnerAccept(root, receiptPath, { owner, actor }) {
|
|
200
|
+
if (!receiptPath) return false;
|
|
201
|
+
const file = path.resolve(root, receiptPath);
|
|
202
|
+
try {
|
|
203
|
+
const receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
204
|
+
receipt.accepted = {
|
|
205
|
+
by: actor || owner || null,
|
|
206
|
+
owner: owner || null,
|
|
207
|
+
self_accepted: true,
|
|
208
|
+
via: 'atris done',
|
|
209
|
+
note: 'the human owner accepted their own work',
|
|
210
|
+
at: new Date().toISOString(),
|
|
211
|
+
};
|
|
212
|
+
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`, 'utf8');
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function renderCredits({ title, built, checked, receipt, next }) {
|
|
220
|
+
return [
|
|
221
|
+
'',
|
|
222
|
+
` ${title}`,
|
|
223
|
+
'',
|
|
224
|
+
` built ${spokenBuiltList(built)}`,
|
|
225
|
+
` checked ${checked}, passed`,
|
|
226
|
+
` signed ${receipt || 'the task record'}`,
|
|
227
|
+
'',
|
|
228
|
+
`next: ${next}`,
|
|
229
|
+
].join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function emit(asJson, log, payload, text) {
|
|
233
|
+
if (asJson) log(JSON.stringify(payload, null, 2));
|
|
234
|
+
else log(text);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function doneAtris(args = process.argv.slice(3), {
|
|
238
|
+
root = process.cwd(),
|
|
239
|
+
log = console.log,
|
|
240
|
+
builder,
|
|
241
|
+
} = {}) {
|
|
242
|
+
const list = Array.isArray(args) ? args : [];
|
|
243
|
+
if (argsWantHelp(list)) {
|
|
244
|
+
log(doneUsage());
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
const asJson = list.includes('--json');
|
|
248
|
+
const ref = list.find((arg) => !String(arg).startsWith('-')) || '';
|
|
249
|
+
|
|
250
|
+
if (isFreshWorkspace(root)) {
|
|
251
|
+
return speakFirstMinute({ root, fresh: true, asJson, log });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const person = personName();
|
|
255
|
+
const tasks = loadLocalTasks(root);
|
|
256
|
+
const task = ref ? findTask(tasks, ref) : pickTask(tasks, person);
|
|
257
|
+
|
|
258
|
+
if (!task) {
|
|
259
|
+
const minute = buildFirstMinute({ root, builder });
|
|
260
|
+
const reason = ref ? `nothing here is called ${ref}` : 'nothing of yours is in progress here';
|
|
261
|
+
// Already shipped: say so, the same way bare atris does.
|
|
262
|
+
const text = !ref && minute.shipped ? `\n${minute.text}` : `\n${reason}.\n\nnext: ${minute.nextCommand}`;
|
|
263
|
+
emit(asJson, log, {
|
|
264
|
+
schema: DONE_SCHEMA,
|
|
265
|
+
ok: false,
|
|
266
|
+
status: 'stuck',
|
|
267
|
+
reason,
|
|
268
|
+
next_action: minute.nextCommand,
|
|
269
|
+
}, text);
|
|
270
|
+
return asJson ? 2 : 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const title = spokenTitle(task);
|
|
274
|
+
if (task.status === 'done') {
|
|
275
|
+
emit(asJson, log, {
|
|
276
|
+
schema: DONE_SCHEMA,
|
|
277
|
+
ok: true,
|
|
278
|
+
status: 'already_done',
|
|
279
|
+
reason: `${title} is already done`,
|
|
280
|
+
next_action: buildFirstMinute({ root, builder }).nextCommand,
|
|
281
|
+
}, `\n${title} is already done.\n\nnext: ${buildFirstMinute({ root, builder }).nextCommand}`);
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
if (!['open', 'claimed', 'review'].includes(task.status)) {
|
|
285
|
+
emit(asJson, log, {
|
|
286
|
+
schema: DONE_SCHEMA,
|
|
287
|
+
ok: false,
|
|
288
|
+
status: 'stuck',
|
|
289
|
+
reason: `${title} is ${task.status}`,
|
|
290
|
+
next_action: `atris task show ${taskRefOf(task)}`,
|
|
291
|
+
}, `\n${title} is ${task.status}.\n\nnext: atris task show ${taskRefOf(task)}`);
|
|
292
|
+
return asJson ? 2 : 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const built = orderBuilt(listBuiltSince(root, taskStartedAt(task)));
|
|
296
|
+
if (!built.length) {
|
|
297
|
+
const next = buildCommandFor(task, builder);
|
|
298
|
+
emit(asJson, log, {
|
|
299
|
+
schema: DONE_SCHEMA,
|
|
300
|
+
ok: true,
|
|
301
|
+
status: 'nothing_new',
|
|
302
|
+
reason: `nothing new here since you started ${title}`,
|
|
303
|
+
task: { display_id: task.display_id || null, title: task.title },
|
|
304
|
+
built: [],
|
|
305
|
+
next_action: next,
|
|
306
|
+
}, `\nnothing new here since you started ${title}.\n\nnext: ${next}`);
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const verify = verifierFor(root, built);
|
|
311
|
+
const fail = (reason, next) => {
|
|
312
|
+
emit(asJson, log, {
|
|
313
|
+
schema: DONE_SCHEMA,
|
|
314
|
+
ok: false,
|
|
315
|
+
status: 'stuck',
|
|
316
|
+
reason,
|
|
317
|
+
task: { display_id: task.display_id || null, title: task.title },
|
|
318
|
+
built,
|
|
319
|
+
next_action: next,
|
|
320
|
+
}, `\n${title} has new work, but ${reason}.\n\nnext: ${next}`);
|
|
321
|
+
return 1;
|
|
322
|
+
};
|
|
323
|
+
if (!verify) return fail('nothing built has content to check yet', buildCommandFor(task, builder));
|
|
324
|
+
|
|
325
|
+
let current = liveTask(root, task.id) || task;
|
|
326
|
+
if (current.status !== 'review') {
|
|
327
|
+
const ready = runAtris(root, [
|
|
328
|
+
'task', 'ready', task.id,
|
|
329
|
+
'--verify', verify.command,
|
|
330
|
+
'--result', resultSentence(task),
|
|
331
|
+
'--json',
|
|
332
|
+
]);
|
|
333
|
+
if (!ready.ok) {
|
|
334
|
+
return fail(`the check did not pass (${failureLine(ready)})`, `atris task show ${taskRefOf(task)}`);
|
|
335
|
+
}
|
|
336
|
+
current = liveTask(root, task.id) || current;
|
|
337
|
+
}
|
|
338
|
+
const receipt = receiptFromProof(current && current.metadata && current.metadata.latest_agent_proof);
|
|
339
|
+
|
|
340
|
+
const accept = runAtris(root, ['task', 'accept', task.id, '--json']);
|
|
341
|
+
if (!accept.ok) {
|
|
342
|
+
const why = failureLine(accept);
|
|
343
|
+
const waiting = /agent_proof_only|cannot accept/i.test(`${why} ${accept.stderr} ${accept.stdout}`);
|
|
344
|
+
return fail(
|
|
345
|
+
waiting ? 'it is checked and waiting on a person to accept it' : `accept did not go through (${why})`,
|
|
346
|
+
`atris task accept ${taskRefOf(task)}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const actor = (accept.json && accept.json.episode && (accept.json.episode.actor || accept.json.episode.reviewer)) || '';
|
|
350
|
+
const stamped = stampOwnerAccept(root, receipt, { owner: task.claimed_by || person, actor });
|
|
351
|
+
|
|
352
|
+
const main = mainBuiltFile(built);
|
|
353
|
+
const next = openCommand(main);
|
|
354
|
+
emit(asJson, log, {
|
|
355
|
+
schema: DONE_SCHEMA,
|
|
356
|
+
ok: true,
|
|
357
|
+
status: 'done',
|
|
358
|
+
reason: `${title} is done`,
|
|
359
|
+
task: { display_id: task.display_id || null, title: task.title },
|
|
360
|
+
built,
|
|
361
|
+
checked: { command: verify.command, plain: verify.plain, passed: true },
|
|
362
|
+
receipt: receipt || null,
|
|
363
|
+
owner_accepted: stamped,
|
|
364
|
+
next_action: next,
|
|
365
|
+
}, renderCredits({ title, built, checked: verify.plain, receipt, next }));
|
|
366
|
+
return 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
module.exports = {
|
|
370
|
+
doneAtris,
|
|
371
|
+
resultSentence,
|
|
372
|
+
verifierFor,
|
|
373
|
+
openCommand,
|
|
374
|
+
};
|
package/commands/member.js
CHANGED
|
@@ -5889,6 +5889,26 @@ function fallbackSupervisorAnalysis(receipts) {
|
|
|
5889
5889
|
async function runSupervisorWake(name, paths, { execute = false } = {}) {
|
|
5890
5890
|
const mode = execute ? 'execute' : 'dry_run';
|
|
5891
5891
|
const root = process.cwd();
|
|
5892
|
+
const llmConfigured = Boolean(process.env.ATRIS_SUPERVISOR_LLM_JSON)
|
|
5893
|
+
|| process.env.ATRIS_SUPERVISOR_LLM === '1';
|
|
5894
|
+
if (!execute && !llmConfigured) {
|
|
5895
|
+
return {
|
|
5896
|
+
ok: true,
|
|
5897
|
+
skipped: true,
|
|
5898
|
+
action: 'wake',
|
|
5899
|
+
member: name,
|
|
5900
|
+
mode,
|
|
5901
|
+
decision: 'wait',
|
|
5902
|
+
reason: 'skipped_llm_not_configured',
|
|
5903
|
+
executed: false,
|
|
5904
|
+
needs_user: false,
|
|
5905
|
+
ask: null,
|
|
5906
|
+
next_command: null,
|
|
5907
|
+
receipt_path: null,
|
|
5908
|
+
log_path: null,
|
|
5909
|
+
recommendations_path: supervisorRecommendationsPath(root),
|
|
5910
|
+
};
|
|
5911
|
+
}
|
|
5892
5912
|
const receipts = listRecentSupervisorReceipts(root);
|
|
5893
5913
|
const logs = readSupervisorMemberLogs(root);
|
|
5894
5914
|
let llm = null;
|
|
@@ -8139,6 +8159,7 @@ const WAKE_REASON_TEXT = {
|
|
|
8139
8159
|
heuristic_objective_proposal_dry_run: 'it previewed an objective proposal using its built-in heuristics, without writing it',
|
|
8140
8160
|
install_requires_clean_git: 'installing needs a clean git tree first',
|
|
8141
8161
|
insufficient_world_model_data: 'its world model is too thin to act on yet',
|
|
8162
|
+
skipped_llm_not_configured: 'no model is configured here, so it skipped quietly instead of writing another dry-run receipt',
|
|
8142
8163
|
llm_json_parse_failed: 'the model reply did not parse, so it stopped rather than act on garbage',
|
|
8143
8164
|
llm_json_parse_failed_heuristic_used: 'the model reply did not parse, so it fell back to built-in heuristics',
|
|
8144
8165
|
missing_domain_input: 'it needs a domain file or domain text from you to work on',
|
|
@@ -8231,7 +8252,9 @@ function wakeBootLines(name, result) {
|
|
|
8231
8252
|
|| /^open_experiment_/.test(result.reason || '');
|
|
8232
8253
|
lines.push('', ` ${s.bold(ballWithHuman ? 'Your move' : 'Next step')}`, ` ${s.cyan(result.next_command)}`);
|
|
8233
8254
|
}
|
|
8234
|
-
|
|
8255
|
+
if (result.receipt_path) {
|
|
8256
|
+
lines.push('', ` ${s.dim(`receipt \u00b7 ${path.relative(process.cwd(), result.receipt_path)}`)}`, '');
|
|
8257
|
+
}
|
|
8235
8258
|
return lines;
|
|
8236
8259
|
}
|
|
8237
8260
|
|
package/commands/workflow.js
CHANGED
|
@@ -83,7 +83,7 @@ function reviewSoftTitle(title, maxWords = 5) {
|
|
|
83
83
|
|
|
84
84
|
function isHumanDeskNext(command) {
|
|
85
85
|
const text = String(command || '');
|
|
86
|
-
return /^atris do\b/.test(text) || /^atris task (?:claim|show|ready|accept)\b/.test(text);
|
|
86
|
+
return /^atris do\b/.test(text) || /^atris done\b/.test(text) || /^atris task (?:claim|show|ready|accept)\b/.test(text);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
function loadReviewTasks(root = process.cwd()) {
|
package/commands/youtube.js
CHANGED
|
@@ -1021,16 +1021,26 @@ function runYoutubeUnsave(args = [], deps = {}) {
|
|
|
1021
1021
|
|
|
1022
1022
|
function ensureProcessApply({ cwd, url, now, output } = {}) {
|
|
1023
1023
|
const id = videoIdFromUrl(url);
|
|
1024
|
-
|
|
1024
|
+
const rel = id ? applySidecarRel(id) : null;
|
|
1025
|
+
const code = applyGate.ensureApply({
|
|
1025
1026
|
cwd,
|
|
1026
1027
|
source: url,
|
|
1027
|
-
rel
|
|
1028
|
+
rel,
|
|
1028
1029
|
now,
|
|
1029
1030
|
output,
|
|
1030
1031
|
incompleteMessage: PROCESS_APPLY_MESSAGE,
|
|
1031
1032
|
required: true,
|
|
1032
1033
|
human: true,
|
|
1033
1034
|
});
|
|
1035
|
+
if (code !== 0 && rel) {
|
|
1036
|
+
// Name the file so the gate is passable, including in a workspace with no atris/wiki yet.
|
|
1037
|
+
const print = typeof output === 'function' ? output : (line = '') => console.error(line);
|
|
1038
|
+
const exists = Boolean(cwd) && fs.existsSync(path.join(cwd, rel));
|
|
1039
|
+
print(exists
|
|
1040
|
+
? `next: fill change: and receipt: in ${rel}, then rerun process`
|
|
1041
|
+
: `next: create ${rel} with change: and receipt: lines, then rerun process`);
|
|
1042
|
+
}
|
|
1043
|
+
return code;
|
|
1034
1044
|
}
|
|
1035
1045
|
|
|
1036
1046
|
function processExperimentSlug(id) {
|
package/lib/falsifier-probe.js
CHANGED
|
@@ -47,10 +47,16 @@ function probeVerifierCanFail({ command, runner, tmpRoot } = {}) {
|
|
|
47
47
|
const dir = fs.mkdtempSync(path.join(tmpRoot || os.tmpdir(), 'atris-falsifier-probe-'));
|
|
48
48
|
try {
|
|
49
49
|
const run = runner || spawnSync;
|
|
50
|
+
// A probe that inherits a test-runner context joins the outer runner as a
|
|
51
|
+
// child and exits 0 without running anything, so every check would look
|
|
52
|
+
// like it cannot fail. Same strip as verifyCommandEnv.
|
|
53
|
+
const env = { ...process.env };
|
|
54
|
+
delete env.NODE_TEST_CONTEXT;
|
|
50
55
|
const result = run('bash', ['-lc', cmd], {
|
|
51
56
|
cwd: dir,
|
|
52
57
|
encoding: 'utf8',
|
|
53
58
|
timeout: PROBE_TIMEOUT_MS,
|
|
59
|
+
env,
|
|
54
60
|
});
|
|
55
61
|
if (result.error) {
|
|
56
62
|
const timedOut = /ETIMEDOUT/i.test(String(result.error.code || result.error.message || ''));
|
package/lib/first-minute.js
CHANGED
|
@@ -272,6 +272,25 @@ function taskNextCommand(tasks, person) {
|
|
|
272
272
|
return picked.command;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
// A person's first talk mints an onboarding task in their own words. That task
|
|
276
|
+
// is the human's thing; the init seed ("Generate MAP.md, ...") is agent setup.
|
|
277
|
+
function isHumanStarter(task) {
|
|
278
|
+
const metadata = (task && task.metadata) || {};
|
|
279
|
+
return Boolean(task) && (metadata.source === 'context_gatherer' || task.tag === 'onboarding');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isInitSeedTask(task) {
|
|
283
|
+
return Boolean(task && task.metadata && task.metadata.source === 'init_seed');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// In a room a person started by talking, the open init seed is not their next
|
|
287
|
+
// step. Agents still find it on the task plane; the human screens skip it.
|
|
288
|
+
function withoutSeedForHumans(tasks) {
|
|
289
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
290
|
+
if (!list.some(isHumanStarter)) return list;
|
|
291
|
+
return list.filter((task) => !(isInitSeedTask(task) && task.status === 'open'));
|
|
292
|
+
}
|
|
293
|
+
|
|
275
294
|
function pickNext({
|
|
276
295
|
tasks = [],
|
|
277
296
|
missions = [],
|
|
@@ -281,6 +300,7 @@ function pickNext({
|
|
|
281
300
|
inboxCount = 0,
|
|
282
301
|
wipCount = 0,
|
|
283
302
|
} = {}) {
|
|
303
|
+
tasks = withoutSeedForHumans(tasks);
|
|
284
304
|
const reviews = (Array.isArray(tasks) ? tasks : []).filter((task) => task && task.status === 'review');
|
|
285
305
|
const certified = newest(reviews.filter(isCertifiedReview));
|
|
286
306
|
if (certified) return { task: certified, command: taskCommand(certified, person) };
|
|
@@ -361,6 +381,94 @@ function spokenVisibleNames(files) {
|
|
|
361
381
|
return names.sort((a, b) => a.localeCompare(b));
|
|
362
382
|
}
|
|
363
383
|
|
|
384
|
+
const BUILT_SKIP_DIRS = new Set(['atris', 'node_modules']);
|
|
385
|
+
const BUILT_MAX_FILES = 500;
|
|
386
|
+
const BUILT_MAX_DEPTH = 6;
|
|
387
|
+
|
|
388
|
+
function toMs(value) {
|
|
389
|
+
if (value == null || value === '') return 0;
|
|
390
|
+
const n = Number(value);
|
|
391
|
+
if (Number.isFinite(n)) return n;
|
|
392
|
+
const parsed = Date.parse(String(value));
|
|
393
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function taskStartedAt(task) {
|
|
397
|
+
return toMs(task && task.claimed_at) || toMs(task && task.created_at);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Files a person can see that were written after `sinceMs`: what their coding
|
|
401
|
+
// agent built. atris/, .atris/, node_modules, and dotfiles are not the work.
|
|
402
|
+
function listBuiltSince(root = process.cwd(), sinceMs = 0) {
|
|
403
|
+
const base = path.resolve(root || '.');
|
|
404
|
+
const since = Number(sinceMs) || 0;
|
|
405
|
+
const found = [];
|
|
406
|
+
let seen = 0;
|
|
407
|
+
const walk = (dir, rel, depth) => {
|
|
408
|
+
if (depth > BUILT_MAX_DEPTH || seen >= BUILT_MAX_FILES) return;
|
|
409
|
+
let entries = [];
|
|
410
|
+
try {
|
|
411
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
412
|
+
} catch {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
for (const entry of entries) {
|
|
416
|
+
if (seen >= BUILT_MAX_FILES) return;
|
|
417
|
+
const name = entry.name;
|
|
418
|
+
if (!isUserVisibleName(name) || name === LATER_NOTE_FILE) continue;
|
|
419
|
+
if (depth === 0 && BUILT_SKIP_DIRS.has(name)) continue;
|
|
420
|
+
if (name === 'node_modules') continue;
|
|
421
|
+
const full = path.join(dir, name);
|
|
422
|
+
const relPath = rel ? `${rel}/${name}` : name;
|
|
423
|
+
if (entry.isDirectory()) {
|
|
424
|
+
walk(full, relPath, depth + 1);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (!entry.isFile()) continue;
|
|
428
|
+
seen += 1;
|
|
429
|
+
let stat;
|
|
430
|
+
try {
|
|
431
|
+
stat = fs.statSync(full);
|
|
432
|
+
} catch {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (stat.mtimeMs >= since) found.push(relPath);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
walk(base, '', 0);
|
|
439
|
+
return found.sort((a, b) => a.localeCompare(b));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// The thing to open: an index.html (shallowest first), else the first file.
|
|
443
|
+
function mainBuiltFile(files) {
|
|
444
|
+
const list = Array.isArray(files) ? files : [];
|
|
445
|
+
const pages = list
|
|
446
|
+
.filter((file) => /(^|\/)index\.html?$/i.test(file))
|
|
447
|
+
.sort((a, b) => a.split('/').length - b.split('/').length || a.localeCompare(b));
|
|
448
|
+
return pages[0] || list[0] || '';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function orderBuilt(files) {
|
|
452
|
+
const list = Array.isArray(files) ? [...files] : [];
|
|
453
|
+
const main = mainBuiltFile(list);
|
|
454
|
+
if (!main) return list;
|
|
455
|
+
return [main, ...list.filter((file) => file !== main)];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function spokenBuiltList(files, max = 3) {
|
|
459
|
+
const list = orderBuilt(files);
|
|
460
|
+
if (list.length <= max) return list.join(', ');
|
|
461
|
+
return `${list.slice(0, max).join(', ')} and ${list.length - max} more`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function humanStarterTask(tasks, person = '', { statuses = ['claimed', 'review'] } = {}) {
|
|
465
|
+
const list = (Array.isArray(tasks) ? tasks : [])
|
|
466
|
+
.filter((task) => isHumanStarter(task) && statuses.includes(task.status));
|
|
467
|
+
const who = String(person || '').trim();
|
|
468
|
+
const mine = who ? list.filter((task) => String(task.claimed_by || '') === who) : [];
|
|
469
|
+
return newest(mine.length ? mine : list);
|
|
470
|
+
}
|
|
471
|
+
|
|
364
472
|
function spokenCommitSubject(value) {
|
|
365
473
|
const text = clip(value, 72);
|
|
366
474
|
if (text.endsWith('...')) return text;
|
|
@@ -619,13 +727,85 @@ function firstTalkJson({ starter = null, person = '', folder = 'this folder' } =
|
|
|
619
727
|
};
|
|
620
728
|
}
|
|
621
729
|
|
|
622
|
-
|
|
730
|
+
// First talk only saves the want as a task. Say so, and make the one next step
|
|
731
|
+
// the local coding agent that builds it, so a new person is never told work
|
|
732
|
+
// exists when none does. Test runners get no probe (machine-independent output).
|
|
733
|
+
const FIRST_TALK_BUILDERS = ['claude', 'codex', 'cursor-agent'];
|
|
734
|
+
const FIRST_TALK_INSTALL = 'npm install -g @anthropic-ai/claude-code';
|
|
735
|
+
|
|
736
|
+
function firstTalkBuilder(probe = (bin) => spawnSync('sh', ['-c', `command -v ${bin}`], { encoding: 'utf8' })) {
|
|
737
|
+
if (process.env.NODE_TEST_CONTEXT && arguments.length === 0) return undefined;
|
|
738
|
+
for (const bin of FIRST_TALK_BUILDERS) {
|
|
739
|
+
const result = probe(bin);
|
|
740
|
+
if (result && result.status === 0 && String(result.stdout || '').trim()) return bin;
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function firstTalkBuildCommand(builder, want) {
|
|
746
|
+
if (builder === undefined) return '';
|
|
747
|
+
if (!builder) return FIRST_TALK_INSTALL;
|
|
748
|
+
const text = String(want || '').replace(/\s+/g, ' ').trim().replace(/(["\\$`])/g, '\\$1');
|
|
749
|
+
return `${builder} "${text}"`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function renderFirstTalk({ person = '', folder = 'this folder', starter = null, builder } = {}) {
|
|
623
753
|
const room = folder || 'this folder';
|
|
624
754
|
const title = spokenStarterTitle(starter && starter.title, room);
|
|
755
|
+
const build = firstTalkBuildCommand(builder, (starter && starter.title) || title);
|
|
625
756
|
return [
|
|
626
|
-
`${greet(person)}${title} is
|
|
757
|
+
`${greet(person)}${title} is started. nothing is built yet.`,
|
|
627
758
|
'',
|
|
628
|
-
`next: ${firstTalkNext({ starter, person, folder: room })}`,
|
|
759
|
+
`next: ${build || firstTalkNext({ starter, person, folder: room })}`,
|
|
760
|
+
].join('\n');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const WHATS_NEXT_COMMAND = 'atris "what\'s next?"';
|
|
764
|
+
|
|
765
|
+
function isWhatsNextLine(value) {
|
|
766
|
+
const text = String(value || '')
|
|
767
|
+
.trim()
|
|
768
|
+
.replace(/^atris\s+/i, '')
|
|
769
|
+
.replace(/^["']+|["']+$/g, '')
|
|
770
|
+
.toLowerCase()
|
|
771
|
+
.replace(/[?.!]+$/g, '')
|
|
772
|
+
.replace(/\s+/g, ' ')
|
|
773
|
+
.trim();
|
|
774
|
+
return /^(what'?s|what is) next$/.test(text) || text === 'what next';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function starterTitle(task, folder = 'this folder') {
|
|
778
|
+
return spokenStarterTitle(clip(task && task.title, 80), folder);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function renderStarterNewWork({ person = '', task = null, files = [], folder = 'this folder' } = {}) {
|
|
782
|
+
return [
|
|
783
|
+
`${greet(person)}${starterTitle(task, folder)} has new work: ${spokenBuiltList(files)}.`,
|
|
784
|
+
'',
|
|
785
|
+
'next: atris done',
|
|
786
|
+
].join('\n');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// The human's first want is landed and nothing else of theirs is live.
|
|
790
|
+
function shippedHumanStarter(tasks) {
|
|
791
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
792
|
+
if (list.some((task) => isHumanStarter(task) && isActionableTask(task))) return null;
|
|
793
|
+
return newest(list.filter((task) => isHumanStarter(task) && task.status === 'done'));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function renderStarterShipped({ person = '', task = null, folder = 'this folder' } = {}) {
|
|
797
|
+
return [
|
|
798
|
+
`${greet(person)}you already shipped ${starterTitle(task, folder)}.`,
|
|
799
|
+
'',
|
|
800
|
+
`next: ${WHATS_NEXT_COMMAND}`,
|
|
801
|
+
].join('\n');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function renderWhatsNextAsk({ person = '', task = null, folder = 'this folder' } = {}) {
|
|
805
|
+
return [
|
|
806
|
+
`${greet(person)}${starterTitle(task, folder)} is shipped. say the next thing you want here, in your own words.`,
|
|
807
|
+
'',
|
|
808
|
+
'next: atris "<the next thing you want>"',
|
|
629
809
|
].join('\n');
|
|
630
810
|
}
|
|
631
811
|
|
|
@@ -694,6 +874,7 @@ function buildFirstMinute({
|
|
|
694
874
|
dirty,
|
|
695
875
|
branch,
|
|
696
876
|
later,
|
|
877
|
+
builder,
|
|
697
878
|
} = {}) {
|
|
698
879
|
const who = person != null ? person : personName();
|
|
699
880
|
const room = folder != null ? folder : folderName(root);
|
|
@@ -718,8 +899,16 @@ function buildFirstMinute({
|
|
|
718
899
|
};
|
|
719
900
|
}
|
|
720
901
|
const tasks = loadLocalTasks(root);
|
|
721
|
-
|
|
722
|
-
|
|
902
|
+
// The rendered TODO.md still lists the init seed; in a person's room it is
|
|
903
|
+
// not their waiting work either.
|
|
904
|
+
const seedTitles = tasks.some(isHumanStarter)
|
|
905
|
+
? new Set(tasks.filter(isInitSeedTask).map((task) => String(task.title || '').trim().toLowerCase()))
|
|
906
|
+
: null;
|
|
907
|
+
const notSeed = (titles) => (titles && seedTitles
|
|
908
|
+
? titles.filter((title) => !seedTitles.has(String(title || '').trim().toLowerCase()))
|
|
909
|
+
: titles);
|
|
910
|
+
const todoBacklog = notSeed(titlesFromTodoSection(root, 'Backlog'));
|
|
911
|
+
const todoWip = notSeed(titlesFromTodoSection(root, 'In Progress'));
|
|
723
912
|
const backlogCount = todoBacklog
|
|
724
913
|
? todoBacklog.length
|
|
725
914
|
: (Array.isArray(context.backlogTasks) ? context.backlogTasks.length : 0);
|
|
@@ -749,6 +938,55 @@ function buildFirstMinute({
|
|
|
749
938
|
wipCount,
|
|
750
939
|
inboxCount,
|
|
751
940
|
});
|
|
941
|
+
// The person's own first want, claimed: if their coding agent built
|
|
942
|
+
// something since, the next step is `atris done`. If nothing is built yet,
|
|
943
|
+
// the next step is the builder, not a task-plane ready line.
|
|
944
|
+
if (picked.task && picked.task.status === 'claimed' && isHumanStarter(picked.task)) {
|
|
945
|
+
const built = listBuiltSince(root, taskStartedAt(picked.task));
|
|
946
|
+
if (built.length) {
|
|
947
|
+
return {
|
|
948
|
+
kind: 'workspace',
|
|
949
|
+
text: renderStarterNewWork({ person: who, task: picked.task, files: built, folder: room }),
|
|
950
|
+
nextCommand: 'atris done',
|
|
951
|
+
task: picked.task,
|
|
952
|
+
recap,
|
|
953
|
+
starter: true,
|
|
954
|
+
built,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const build = firstTalkBuildCommand(
|
|
958
|
+
builder !== undefined ? builder : firstTalkBuilder(),
|
|
959
|
+
picked.task.title,
|
|
960
|
+
);
|
|
961
|
+
if (build) {
|
|
962
|
+
return {
|
|
963
|
+
kind: 'workspace',
|
|
964
|
+
text: renderWorkspace({
|
|
965
|
+
person: who,
|
|
966
|
+
folder: room,
|
|
967
|
+
task: picked.task,
|
|
968
|
+
recap,
|
|
969
|
+
nextCommand: build,
|
|
970
|
+
liveWork,
|
|
971
|
+
}),
|
|
972
|
+
nextCommand: build,
|
|
973
|
+
task: picked.task,
|
|
974
|
+
recap,
|
|
975
|
+
starter: true,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const shipped = !picked.task && !picked.mission && !liveWork ? shippedHumanStarter(tasks) : null;
|
|
980
|
+
if (shipped) {
|
|
981
|
+
return {
|
|
982
|
+
kind: 'workspace',
|
|
983
|
+
text: renderStarterShipped({ person: who, task: shipped, folder: room }),
|
|
984
|
+
nextCommand: WHATS_NEXT_COMMAND,
|
|
985
|
+
task: null,
|
|
986
|
+
recap,
|
|
987
|
+
shipped,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
752
990
|
return {
|
|
753
991
|
kind: 'workspace',
|
|
754
992
|
text: renderWorkspace({
|
|
@@ -863,7 +1101,8 @@ function spokenWinReason(text) {
|
|
|
863
1101
|
|
|
864
1102
|
function isKeepWorkingMinute(minute) {
|
|
865
1103
|
const next = String((minute && minute.nextCommand) || '');
|
|
866
|
-
if (next === 'atris do') return true;
|
|
1104
|
+
if (next === 'atris do' || next === 'atris done') return true;
|
|
1105
|
+
if (minute && (minute.starter || minute.shipped)) return true;
|
|
867
1106
|
return isClaimedKeepWorkingCommand(next);
|
|
868
1107
|
}
|
|
869
1108
|
|
|
@@ -913,12 +1152,24 @@ module.exports = {
|
|
|
913
1152
|
isCertifiedReview,
|
|
914
1153
|
isFirstTalkLine,
|
|
915
1154
|
isFreshWorkspace,
|
|
1155
|
+
isWhatsNextLine,
|
|
1156
|
+
humanStarterTask,
|
|
1157
|
+
listBuiltSince,
|
|
1158
|
+
mainBuiltFile,
|
|
1159
|
+
orderBuilt,
|
|
1160
|
+
shippedHumanStarter,
|
|
1161
|
+
spokenBuiltList,
|
|
1162
|
+
taskStartedAt,
|
|
1163
|
+
loadLocalTasks,
|
|
1164
|
+
firstTalkBuildCommand,
|
|
1165
|
+
renderWhatsNextAsk,
|
|
916
1166
|
isLeftoverVerbLook,
|
|
917
1167
|
isTaskNextLook,
|
|
918
1168
|
listUserVisibleWork,
|
|
919
1169
|
personName,
|
|
920
1170
|
pickNext,
|
|
921
1171
|
renderFirstTalk,
|
|
1172
|
+
firstTalkBuilder,
|
|
922
1173
|
renderLaterRemember,
|
|
923
1174
|
spokenLaterNote,
|
|
924
1175
|
visibleWorkTitle,
|
package/lib/known-commands.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
3
|
+
const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'done', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
5
|
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
6
6
|
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.68.
|
|
3
|
+
"version": "3.68.2",
|
|
4
4
|
"description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
|
|
5
5
|
"main": "bin/atris.js",
|
|
6
6
|
"bin": {
|
|
@@ -101,5 +101,8 @@
|
|
|
101
101
|
"bugs": {
|
|
102
102
|
"url": "https://github.com/atrislabs/atris/issues"
|
|
103
103
|
},
|
|
104
|
-
"homepage": "https://github.com/atrislabs/atris#readme"
|
|
104
|
+
"homepage": "https://github.com/atrislabs/atris#readme",
|
|
105
|
+
"engines": {
|
|
106
|
+
"node": ">=22.13"
|
|
107
|
+
}
|
|
105
108
|
}
|
package/scripts/det/ytnotes
CHANGED
|
@@ -379,8 +379,18 @@ run_haiku() {
|
|
|
379
379
|
# Gemini Flash through the Antigravity CLI (agy). On an 85-minute transcript it
|
|
380
380
|
# writes notes in about 8s; Haiku takes 30-40s. The old gemini CLI is retired.
|
|
381
381
|
# Low effort is the floor for notes: high effort is slower and not sharper here.
|
|
382
|
+
# The Antigravity editor ships its own `agy` (a VS Code-style launcher script).
|
|
383
|
+
# It has no -p mode: it hands -p and --model to Electron and prints nothing, so
|
|
384
|
+
# it is not a writer. Only the Antigravity CLI is.
|
|
385
|
+
agy_is_cli() {
|
|
386
|
+
local bin
|
|
387
|
+
bin=$(command -v agy 2>/dev/null) || return 1
|
|
388
|
+
! head -c 4096 "$bin" 2>/dev/null | grep -q 'VSCODE_IPC_HOOK_CLI'
|
|
389
|
+
}
|
|
390
|
+
|
|
382
391
|
run_gemini() {
|
|
383
392
|
command -v agy >/dev/null 2>&1 || { echo "agy is not installed" >&2; return 127; }
|
|
393
|
+
agy_is_cli || { echo "agy on PATH is the Antigravity editor launcher, which has no -p mode. Install the Antigravity CLI for Gemini notes." >&2; return 127; }
|
|
384
394
|
run_with_timeout 120 agy -p "$PROMPT" --model "${ATRIS_YTNOTES_GEMINI_MODEL:-${1:-gemini-3.8-flash}}" \
|
|
385
395
|
--effort "${2:-low}" --print-timeout 110s </dev/null
|
|
386
396
|
}
|
|
@@ -448,6 +458,7 @@ run_auto() {
|
|
|
448
458
|
for entry in $(writer_list); do
|
|
449
459
|
IFS='|' read -r cmd model effort label <<< "$entry"
|
|
450
460
|
command -v "$cmd" >/dev/null 2>&1 || continue
|
|
461
|
+
if [ "$cmd" = agy ] && ! agy_is_cli; then continue; fi
|
|
451
462
|
if [ -n "$tried" ]; then
|
|
452
463
|
echo "$tried did not answer; writing the notes with $label instead." >&2
|
|
453
464
|
fi
|
package/utils/backend-root.js
CHANGED
|
@@ -24,6 +24,10 @@ function resolveBackendRoot(cwd = process.cwd()) {
|
|
|
24
24
|
|
|
25
25
|
const sibling = path.resolve(workspace, '..', 'atrisos-backend');
|
|
26
26
|
if (fs.existsSync(sibling) && fs.statSync(sibling).isDirectory()) return sibling;
|
|
27
|
+
// Test children inherit NODE_TEST_CONTEXT: do not discover the developer's
|
|
28
|
+
// backend beside this CLI install unless a test explicitly opts in.
|
|
29
|
+
const installSibling = process.env.ATRIS_BACKEND_INSTALL_SIBLING;
|
|
30
|
+
if (installSibling === '0' || (process.env.NODE_TEST_CONTEXT && installSibling !== '1')) return null;
|
|
27
31
|
const installedSibling = path.resolve(__dirname, '..', '..', 'atrisos-backend');
|
|
28
32
|
return fs.existsSync(installedSibling) && fs.statSync(installedSibling).isDirectory() ? installedSibling : null;
|
|
29
33
|
}
|