atris 3.56.0 → 3.56.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/engines/SKILL.md +11 -3
- package/atris/skills/x-search/SKILL.md +20 -1
- package/atris/skills/youtube/SKILL.md +51 -43
- package/bin/atris.js +134 -90
- package/commands/auth.js +6 -1
- package/commands/autopilot-front.js +29 -13
- package/commands/brainstorm.js +77 -476
- package/commands/business.js +13 -1
- package/commands/engine.js +7 -0
- package/commands/experiments.js +178 -0
- package/commands/fleet-report.js +2 -2
- package/commands/founder.js +12 -0
- package/commands/human-missions.js +64 -2
- package/commands/init.js +2 -35
- package/commands/integrations.js +100 -0
- package/commands/land.js +53 -23
- package/commands/later.js +52 -0
- package/commands/launchpad.js +45 -10
- package/commands/log.js +79 -30
- package/commands/mission.js +117 -22
- package/commands/next.js +153 -63
- package/commands/now.js +115 -38
- package/commands/recap.js +97 -4
- package/commands/run-front.js +20 -5
- package/commands/spaceship.js +52 -12
- package/commands/status.js +38 -7
- package/commands/task.js +56 -19
- package/commands/terminal.js +5 -5
- package/commands/workflow.js +19 -5
- package/commands/x-search.js +123 -13
- package/commands/youtube.js +941 -36
- package/lib/account-bound.js +7 -0
- package/lib/apply-gate.js +102 -0
- package/lib/config-guard.js +106 -0
- package/lib/context-gatherer.js +55 -8
- package/lib/engine-ask.js +16 -6
- package/lib/engine-registry.js +14 -4
- package/lib/first-minute.js +571 -26
- package/lib/known-commands.js +1 -1
- package/lib/pack-capabilities.js +13 -3
- package/lib/runner-command.js +5 -3
- package/lib/scratch-root.js +44 -0
- package/lib/workspace-scaffold.js +3 -1
- package/package.json +1 -1
- package/utils/auth.js +84 -6
package/commands/business.js
CHANGED
|
@@ -3277,11 +3277,14 @@ async function deployBusiness(slug) {
|
|
|
3277
3277
|
// Upload workspace files
|
|
3278
3278
|
const workspaceDir = path.join(bizDir, 'workspace');
|
|
3279
3279
|
let uploadCount = 0;
|
|
3280
|
+
let uploadFailureCount = 0;
|
|
3281
|
+
let uploadTotal = 0;
|
|
3280
3282
|
if (fs.existsSync(workspaceDir)) {
|
|
3281
3283
|
const files = walkDir(workspaceDir);
|
|
3282
3284
|
for (const filePath of files) {
|
|
3283
3285
|
const relativePath = path.relative(workspaceDir, filePath);
|
|
3284
3286
|
if (relativePath.startsWith('.')) continue;
|
|
3287
|
+
uploadTotal++;
|
|
3285
3288
|
try {
|
|
3286
3289
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
3287
3290
|
const uploadResult = await apiRequestJson(
|
|
@@ -3291,9 +3294,14 @@ async function deployBusiness(slug) {
|
|
|
3291
3294
|
if (uploadResult.ok) {
|
|
3292
3295
|
uploadCount++;
|
|
3293
3296
|
process.stdout.write(` Uploaded: ${relativePath}\n`);
|
|
3297
|
+
} else {
|
|
3298
|
+
uploadFailureCount++;
|
|
3299
|
+
const detail = uploadResult.errorMessage || uploadResult.error || uploadResult.status || 'unknown error';
|
|
3300
|
+
console.log(` Failed: ${relativePath} (${detail})`);
|
|
3294
3301
|
}
|
|
3295
3302
|
} catch (e) {
|
|
3296
|
-
|
|
3303
|
+
uploadFailureCount++;
|
|
3304
|
+
console.log(` Failed: ${relativePath} (${e.message || String(e)})`);
|
|
3297
3305
|
}
|
|
3298
3306
|
}
|
|
3299
3307
|
}
|
|
@@ -3314,6 +3322,10 @@ async function deployBusiness(slug) {
|
|
|
3314
3322
|
|
|
3315
3323
|
console.log(`\n Deployed ${uploadCount} files to ${bizConfig.name}`);
|
|
3316
3324
|
console.log(` Dashboard: https://atris.ai/dashboard/gm/${bizConfig.business_id}`);
|
|
3325
|
+
if (uploadFailureCount > 0) {
|
|
3326
|
+
console.log(` ${uploadFailureCount} of ${uploadTotal} files failed to upload`);
|
|
3327
|
+
process.exitCode = 1;
|
|
3328
|
+
}
|
|
3317
3329
|
console.log('');
|
|
3318
3330
|
}
|
|
3319
3331
|
|
package/commands/engine.js
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
buildRunnerCommand,
|
|
26
26
|
} = require('../lib/runner-command');
|
|
27
27
|
const { parseScopeFlag } = require('../lib/cli-scope');
|
|
28
|
+
const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
|
|
28
29
|
const {
|
|
29
30
|
ENGINE_ROLES,
|
|
30
31
|
ENGINE_DUTIES,
|
|
@@ -1541,6 +1542,9 @@ function engineCommand(args = [], deps = {}) {
|
|
|
1541
1542
|
}
|
|
1542
1543
|
|
|
1543
1544
|
if (sub === 'chart' || scope.args.includes('--chart') || args.includes('--chart')) {
|
|
1545
|
+
if (isFreshWorkspace(root)) {
|
|
1546
|
+
return speakFirstMinute({ root, fresh: true, asJson: json });
|
|
1547
|
+
}
|
|
1544
1548
|
printEngineChart(root);
|
|
1545
1549
|
return 0;
|
|
1546
1550
|
}
|
|
@@ -1551,6 +1555,9 @@ function engineCommand(args = [], deps = {}) {
|
|
|
1551
1555
|
console.log('\n atris engine roster + current default\n atris engines --chart show the fleet as an org chart\n atris engine list --json full registry: default + engines with tier, roles, fallback, health\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> [--json]\n choose the best ready engine for navigator|executor|validator\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
1552
1556
|
return 0;
|
|
1553
1557
|
}
|
|
1558
|
+
if (isFreshWorkspace(root)) {
|
|
1559
|
+
return speakFirstMinute({ root, fresh: true, asJson: json });
|
|
1560
|
+
}
|
|
1554
1561
|
if (json) {
|
|
1555
1562
|
console.log(JSON.stringify(registryPayload(root, { scope: scope.kind }), null, 2));
|
|
1556
1563
|
return 0;
|
package/commands/experiments.js
CHANGED
|
@@ -138,6 +138,176 @@ function runPython(scriptPath, args = [], cwd = process.cwd()) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
function parseMeasureScore(stdout) {
|
|
142
|
+
const line = String(stdout || '').trim().split(/\r?\n/).filter(Boolean).pop();
|
|
143
|
+
if (!line) return { ok: false, error: 'measure.py printed no score' };
|
|
144
|
+
try {
|
|
145
|
+
const payload = JSON.parse(line);
|
|
146
|
+
const score = Number(payload.score);
|
|
147
|
+
if (!Number.isFinite(score)) return { ok: false, error: 'measure.py printed no score' };
|
|
148
|
+
return { ok: true, score, payload };
|
|
149
|
+
} catch {
|
|
150
|
+
return { ok: false, error: 'measure.py printed no score' };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function runMeasureJson(scriptPath, workspaceDir = process.cwd()) {
|
|
155
|
+
const python = resolvePython();
|
|
156
|
+
if (!python) {
|
|
157
|
+
return { ok: false, error: 'python not found. set ATRIS_EXPERIMENTS_PYTHON or install python3.' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const result = spawnSync(python, [scriptPath], {
|
|
161
|
+
cwd: workspaceDir,
|
|
162
|
+
encoding: 'utf8',
|
|
163
|
+
env: {
|
|
164
|
+
...process.env,
|
|
165
|
+
PYTHONDONTWRITEBYTECODE: '1',
|
|
166
|
+
ATRIS_REPO_ROOT: workspaceDir,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (result.error) {
|
|
171
|
+
return { ok: false, error: result.error.message || 'measure.py failed to start' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const parsed = parseMeasureScore(result.stdout);
|
|
175
|
+
if (!parsed.ok) {
|
|
176
|
+
const detail = String(result.stderr || '').trim().split(/\r?\n/).filter(Boolean).pop();
|
|
177
|
+
return { ok: false, error: detail || parsed.error };
|
|
178
|
+
}
|
|
179
|
+
return parsed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function experimentsKeep(name) {
|
|
183
|
+
const token = String(name || '').trim();
|
|
184
|
+
if (!token || token === '--help' || token === '-h' || token === 'help') {
|
|
185
|
+
console.error('usage: atris experiments keep <slug>');
|
|
186
|
+
process.exit(2);
|
|
187
|
+
}
|
|
188
|
+
if (!SLUG_RE.test(token)) {
|
|
189
|
+
console.error('invalid experiment name. use a lowercase-hyphen slug.');
|
|
190
|
+
process.exit(2);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const workspaceDir = process.cwd();
|
|
194
|
+
if (!fs.existsSync(path.join(workspaceDir, 'atris'))) {
|
|
195
|
+
console.error('atris/ folder not found. run "atris init" first.');
|
|
196
|
+
process.exit(2);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const packDir = path.join(workspaceDir, 'atris', 'experiments', token);
|
|
200
|
+
if (!fs.existsSync(packDir) || !fs.statSync(packDir).isDirectory()) {
|
|
201
|
+
console.error(`experiment "${token}" not found.`);
|
|
202
|
+
process.exit(2);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const measurePath = path.join(packDir, 'measure.py');
|
|
206
|
+
if (!fs.existsSync(measurePath)) {
|
|
207
|
+
console.error(`experiment "${token}" has no measure.py.`);
|
|
208
|
+
process.exit(2);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Change is already on disk. Minted packs start at 0; keep only if current score is 1.
|
|
212
|
+
const measured = runMeasureJson(measurePath, workspaceDir);
|
|
213
|
+
if (!measured.ok) {
|
|
214
|
+
console.error(`revert ${token}: ${measured.error}. refuse keep.`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (measured.score === 1) {
|
|
219
|
+
console.log(`keep ${token}: measure.py moved 0→1`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const stayed = measured.score === 0 ? '0' : String(measured.score);
|
|
224
|
+
console.error(`revert ${token}: measure.py stayed ${stayed}. refuse keep.`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function lastProcessLine(result) {
|
|
229
|
+
const text = `${result && result.stderr ? result.stderr : ''}\n${result && result.stdout ? result.stdout : ''}`;
|
|
230
|
+
return text.trim().split(/\r?\n/).filter(Boolean).pop() || '';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function runResetScript(scriptPath, workspaceDir = process.cwd()) {
|
|
234
|
+
const python = resolvePython();
|
|
235
|
+
if (!python) {
|
|
236
|
+
return { ok: false, error: 'python not found. set ATRIS_EXPERIMENTS_PYTHON or install python3.' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = spawnSync(python, [scriptPath], {
|
|
240
|
+
cwd: workspaceDir,
|
|
241
|
+
encoding: 'utf8',
|
|
242
|
+
env: {
|
|
243
|
+
...process.env,
|
|
244
|
+
PYTHONDONTWRITEBYTECODE: '1',
|
|
245
|
+
ATRIS_REPO_ROOT: workspaceDir,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (result.error) {
|
|
250
|
+
return { ok: false, error: result.error.message || 'reset.py failed to start' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
254
|
+
return { ok: false, error: lastProcessLine(result) || `reset.py exited ${result.status}` };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (result.status == null) {
|
|
258
|
+
return { ok: false, error: lastProcessLine(result) || 'reset.py failed' };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { ok: true };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function experimentsRevert(name) {
|
|
265
|
+
const token = String(name || '').trim();
|
|
266
|
+
if (!token || token === '--help' || token === '-h' || token === 'help') {
|
|
267
|
+
console.error('usage: atris experiments revert <slug>');
|
|
268
|
+
process.exit(2);
|
|
269
|
+
}
|
|
270
|
+
if (!SLUG_RE.test(token)) {
|
|
271
|
+
console.error('invalid experiment name. use a lowercase-hyphen slug.');
|
|
272
|
+
process.exit(2);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const workspaceDir = process.cwd();
|
|
276
|
+
if (!fs.existsSync(path.join(workspaceDir, 'atris'))) {
|
|
277
|
+
console.error('atris/ folder not found. run "atris init" first.');
|
|
278
|
+
process.exit(2);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const packDir = path.join(workspaceDir, 'atris', 'experiments', token);
|
|
282
|
+
if (!fs.existsSync(packDir) || !fs.statSync(packDir).isDirectory()) {
|
|
283
|
+
console.error(`experiment "${token}" not found.`);
|
|
284
|
+
process.exit(2);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const resetPath = path.join(packDir, 'reset.py');
|
|
288
|
+
if (!fs.existsSync(resetPath)) {
|
|
289
|
+
console.error(`experiment "${token}" has no reset.py.`);
|
|
290
|
+
process.exit(2);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const reset = runResetScript(resetPath, workspaceDir);
|
|
294
|
+
if (!reset.ok) {
|
|
295
|
+
console.error(`revert ${token}: ${reset.error}. refuse revert.`);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const measurePath = path.join(packDir, 'measure.py');
|
|
300
|
+
if (fs.existsSync(measurePath)) {
|
|
301
|
+
const measured = runMeasureJson(measurePath, workspaceDir);
|
|
302
|
+
if (measured.ok) {
|
|
303
|
+
console.log(`revert ${token}: reset.py ran. measure.py ${measured.score}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(`revert ${token}: reset.py ran`);
|
|
309
|
+
}
|
|
310
|
+
|
|
141
311
|
function experimentsInit(name) {
|
|
142
312
|
const { experimentsDir } = ensureExperimentsFramework();
|
|
143
313
|
|
|
@@ -573,6 +743,10 @@ function experimentsCommand(subcommand, ...args) {
|
|
|
573
743
|
return experimentsReplay(args[0] || 'endstate');
|
|
574
744
|
case 'run':
|
|
575
745
|
return experimentsRun(args[0], ...args.slice(1));
|
|
746
|
+
case 'keep':
|
|
747
|
+
return experimentsKeep(args[0]);
|
|
748
|
+
case 'revert':
|
|
749
|
+
return experimentsRevert(args[0]);
|
|
576
750
|
default:
|
|
577
751
|
console.log('');
|
|
578
752
|
console.log('Usage: atris experiments <subcommand> [name]');
|
|
@@ -581,6 +755,8 @@ function experimentsCommand(subcommand, ...args) {
|
|
|
581
755
|
console.log(' init [slug] Prepare atris/experiments/ or scaffold a new pack');
|
|
582
756
|
console.log(' validate [path|slug] Run structural validation on packs or a single pack');
|
|
583
757
|
console.log(' run <slug> Execute a pack or record an Endstate benchmark receipt');
|
|
758
|
+
console.log(' keep <slug> Keep a pack only when measure.py moves 0 to 1');
|
|
759
|
+
console.log(' revert <slug> Run a pack reset.py to restore the working tree');
|
|
584
760
|
console.log(' compare endstate Compare the latest baseline and stack receipts');
|
|
585
761
|
console.log(' replay endstate Validate, dry-run, and compare the public benchmark flow');
|
|
586
762
|
console.log(' benchmark [mode] Run validate/runtime/all benchmark harness');
|
|
@@ -592,6 +768,8 @@ function experimentsCommand(subcommand, ...args) {
|
|
|
592
768
|
console.log(' atris experiments init self-heal');
|
|
593
769
|
console.log(' atris experiments validate');
|
|
594
770
|
console.log(' atris experiments run endstate-baseline --dry-run');
|
|
771
|
+
console.log(' atris experiments keep teach-teach01-s1');
|
|
772
|
+
console.log(' atris experiments revert teach-teach01-s1');
|
|
595
773
|
console.log(' atris experiments compare endstate');
|
|
596
774
|
console.log(' atris experiments replay endstate');
|
|
597
775
|
console.log(' atris experiments benchmark runtime');
|
package/commands/fleet-report.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const { loadCredentials } = require('../utils/auth');
|
|
17
17
|
const { apiRequestJson } = require('../utils/api');
|
|
18
|
-
const { requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
|
|
18
|
+
const { looksLikeBusinessSlug, requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
|
|
19
19
|
const { argsWantHelp, wantsJson } = require('../lib/noninteractive');
|
|
20
20
|
|
|
21
21
|
function sleep(ms) {
|
|
@@ -181,7 +181,7 @@ async function fleetReport() {
|
|
|
181
181
|
const wake = gate.args.includes('--wake');
|
|
182
182
|
const dryRun = gate.args.includes('--dry-run');
|
|
183
183
|
const board = gate.args.includes('--leaderboard');
|
|
184
|
-
const slug = gate.args.find((a) =>
|
|
184
|
+
const slug = gate.args.find((a) => looksLikeBusinessSlug(a));
|
|
185
185
|
|
|
186
186
|
if (!allAlive && !slug && !board) {
|
|
187
187
|
console.log(usage);
|
package/commands/founder.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
|
|
6
7
|
|
|
7
8
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
9
|
const DEFAULT_DAYS = 28;
|
|
@@ -321,6 +322,17 @@ function founderCommand(args = [], options = {}) {
|
|
|
321
322
|
|
|
322
323
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
323
324
|
const currentWorkspace = workspaceRoot(cwd);
|
|
325
|
+
|
|
326
|
+
// Empty folder talks like bare atris. Do not write scorecard.jsonl.
|
|
327
|
+
// After init, founder still lists and scores.
|
|
328
|
+
if (isFreshWorkspace(cwd) || isFreshWorkspace(currentWorkspace)) {
|
|
329
|
+
return speakFirstMinute({
|
|
330
|
+
root: cwd,
|
|
331
|
+
fresh: true,
|
|
332
|
+
asJson: parsed.json,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
324
336
|
const root = parsed.root
|
|
325
337
|
? path.resolve(cwd, parsed.root)
|
|
326
338
|
: (parsed.global ? defaultScanRoot(cwd, currentWorkspace) : currentWorkspace);
|
|
@@ -14,7 +14,15 @@ const {
|
|
|
14
14
|
const { apiRequestJson } = require('../utils/api');
|
|
15
15
|
const { decodeJwtClaims, loadCredentials } = require('../utils/auth');
|
|
16
16
|
const { isHelpToken } = require('../lib/noninteractive');
|
|
17
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
buildFirstMinute,
|
|
19
|
+
isClaimMinute,
|
|
20
|
+
isFreshWorkspace,
|
|
21
|
+
isKeepWorkingMinute,
|
|
22
|
+
speakFirstMinute,
|
|
23
|
+
speakKeepWorkingMinute,
|
|
24
|
+
speakNothingRunning,
|
|
25
|
+
} = require('../lib/first-minute');
|
|
18
26
|
|
|
19
27
|
const PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
|
|
20
28
|
const HUMAN_STATES = Object.freeze({
|
|
@@ -168,7 +176,10 @@ function parseMissionId(args = []) {
|
|
|
168
176
|
|
|
169
177
|
/**
|
|
170
178
|
* Scratch / unbound folders must not drive account-current cloud work.
|
|
171
|
-
*
|
|
179
|
+
* Empty-folder stop talks first-talk instead. After do/claim, nothing
|
|
180
|
+
* running talks keep-working instead. After init, claim-ready talks
|
|
181
|
+
* the claim instead. After a room exists without that keep-working
|
|
182
|
+
* or claim next, require an explicit id: atris stop --mission <id>
|
|
172
183
|
*/
|
|
173
184
|
function refuseUnboundCloudComputer(args = [], options = {}, commandName = 'stop') {
|
|
174
185
|
const root = options.root || process.cwd();
|
|
@@ -634,6 +645,15 @@ async function currentMissionCommand(args = [], options = {}) {
|
|
|
634
645
|
outputCard(card, asJson, options.log || console.log);
|
|
635
646
|
return 0;
|
|
636
647
|
} catch (caught) {
|
|
648
|
+
if (options.fallbackOnMissing) {
|
|
649
|
+
const code = speakFirstMinute({
|
|
650
|
+
root: options.root || process.cwd(),
|
|
651
|
+
asJson,
|
|
652
|
+
log: options.log || console.log,
|
|
653
|
+
});
|
|
654
|
+
if (options.setProcessExitCode !== false) process.exitCode = code;
|
|
655
|
+
return code;
|
|
656
|
+
}
|
|
637
657
|
return reportError(caught, 'show the current mission', asJson, options);
|
|
638
658
|
}
|
|
639
659
|
}
|
|
@@ -729,11 +749,53 @@ async function approveCommand(args, options = {}) {
|
|
|
729
749
|
return changeCurrentMission('approve', {}, args, options);
|
|
730
750
|
}
|
|
731
751
|
|
|
752
|
+
function hasLiveKeepWorkingRun(root = process.cwd()) {
|
|
753
|
+
try {
|
|
754
|
+
const { pickLiveLocalMission } = require('./mission');
|
|
755
|
+
return Boolean(pickLiveLocalMission(root));
|
|
756
|
+
} catch {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
732
761
|
async function stopCommand(args, options = {}) {
|
|
733
762
|
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
734
763
|
(options.log || console.log)('Usage: atris stop [--mission <id>] [--json]');
|
|
735
764
|
return 0;
|
|
736
765
|
}
|
|
766
|
+
const root = options.root || process.cwd();
|
|
767
|
+
const asJson = args.includes('--json');
|
|
768
|
+
const speak = {
|
|
769
|
+
root,
|
|
770
|
+
asJson,
|
|
771
|
+
log: options.log || console.log,
|
|
772
|
+
};
|
|
773
|
+
// Fresh folder: empty talks first-talk. A file already here
|
|
774
|
+
// names that file, same as bare atris. Do not mint a room.
|
|
775
|
+
if (isFreshWorkspace(root) && !parseMissionId(args)) {
|
|
776
|
+
const unbound = refuseUnboundCloudComputer(args, options, 'stop');
|
|
777
|
+
if (unbound) {
|
|
778
|
+
return speakNothingRunning(speak);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// After do/claim, nothing running: same two keep-working
|
|
782
|
+
// lines as bare atris / status / recap. After init, next
|
|
783
|
+
// is claim: same two lines as bare atris / status / now.
|
|
784
|
+
// Not factory cloud-computer. Do not mint a room or bind
|
|
785
|
+
// a computer. A bound computer still stops the current
|
|
786
|
+
// mission.
|
|
787
|
+
if (!parseMissionId(args) && !hasLiveKeepWorkingRun(root)) {
|
|
788
|
+
const unbound = refuseUnboundCloudComputer(args, options, 'stop');
|
|
789
|
+
if (unbound) {
|
|
790
|
+
const minute = buildFirstMinute({ root });
|
|
791
|
+
if (isKeepWorkingMinute(minute)) {
|
|
792
|
+
return speakKeepWorkingMinute(speak);
|
|
793
|
+
}
|
|
794
|
+
if (isClaimMinute(minute)) {
|
|
795
|
+
return speakFirstMinute(speak);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
737
799
|
return changeCurrentMission('stop', {}, args, options);
|
|
738
800
|
}
|
|
739
801
|
|
package/commands/init.js
CHANGED
|
@@ -468,41 +468,8 @@ function initAtris() {
|
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
if (!fs.existsSync(intuitionFile)) {
|
|
474
|
-
fs.writeFileSync(intuitionFile, `# INTUITION.md
|
|
475
|
-
|
|
476
|
-
> Accumulated learnings. Read before major decisions. Update after discoveries.
|
|
477
|
-
|
|
478
|
-
---
|
|
479
|
-
|
|
480
|
-
## Tripwires
|
|
481
|
-
|
|
482
|
-
*Things that seem obvious but break unexpectedly. Check these first when debugging.*
|
|
483
|
-
|
|
484
|
-
- (none yet — add when you hit surprising failures)
|
|
485
|
-
|
|
486
|
-
---
|
|
487
|
-
|
|
488
|
-
## Preferences
|
|
489
|
-
|
|
490
|
-
*Patterns this codebase prefers. Follow these over generic best practices.*
|
|
491
|
-
|
|
492
|
-
- (none yet — add as you discover the codebase style)
|
|
493
|
-
|
|
494
|
-
---
|
|
495
|
-
|
|
496
|
-
## Dead Ends
|
|
497
|
-
|
|
498
|
-
*Approaches tried and abandoned. Don't retry without new information.*
|
|
499
|
-
|
|
500
|
-
- (none yet — log failed approaches so future agents skip them)
|
|
501
|
-
|
|
502
|
-
---
|
|
503
|
-
`);
|
|
504
|
-
markReady('workspace', 'INTUITION.md', '✓ Created INTUITION.md');
|
|
505
|
-
}
|
|
471
|
+
// INTUITION.md was retired into lessons.md. Do not write a parallel lesson file.
|
|
472
|
+
// Leave a user's existing INTUITION.md alone if one is already on disk.
|
|
506
473
|
|
|
507
474
|
// Create lessons.md (feedback loop for learning across features)
|
|
508
475
|
const lessonsFile = path.join(targetDir, 'lessons.md');
|
package/commands/integrations.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* atris gmail inbox [--account <id>] - List recent emails for a mailbox
|
|
6
6
|
* atris gmail read <id> [--account <id>] - Read specific email
|
|
7
7
|
* atris gmail archive <id> [...] [--account <id>] - Archive messages
|
|
8
|
+
* atris gmail verdicts [--account <id>] [--limit N] - List recent archive verdicts
|
|
8
9
|
* atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - Send an email
|
|
9
10
|
* atris gmail voice [account] [--clear] - Edit an account's writing voice
|
|
10
11
|
* atris gmail connect [name] - Connect or reconnect a Gmail account
|
|
@@ -39,6 +40,77 @@ const GMAIL_CONNECT_POLL_MS = 3000;
|
|
|
39
40
|
const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
40
41
|
const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>]';
|
|
41
42
|
const GMAIL_VOICE_USAGE = 'usage: atris gmail voice [account] [--clear]';
|
|
43
|
+
const GMAIL_VERDICTS_USAGE = 'usage: atris gmail verdicts [--account <id>] [--limit N]';
|
|
44
|
+
|
|
45
|
+
function gmailVerdictsPath(root = process.cwd()) {
|
|
46
|
+
return path.join(root, '.atris', 'state', 'gmail-verdicts.jsonl');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appendGmailVerdicts(verdicts, options = {}) {
|
|
50
|
+
const rows = Array.isArray(verdicts) ? verdicts : [];
|
|
51
|
+
if (!rows.length) return;
|
|
52
|
+
const filePath = options.filePath || gmailVerdictsPath(options.root);
|
|
53
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
54
|
+
const lines = rows.map((row) => JSON.stringify({
|
|
55
|
+
ts: row.ts || new Date().toISOString(),
|
|
56
|
+
account: String(row.account || ''),
|
|
57
|
+
verdict: 'archive',
|
|
58
|
+
message_id: String(row.message_id || ''),
|
|
59
|
+
...(row.from ? { from: row.from } : {}),
|
|
60
|
+
...(row.subject ? { subject: row.subject } : {}),
|
|
61
|
+
}));
|
|
62
|
+
fs.appendFileSync(filePath, `${lines.join('\n')}\n`, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readGmailVerdicts(options = {}) {
|
|
66
|
+
const filePath = options.filePath || gmailVerdictsPath(options.root);
|
|
67
|
+
let lines;
|
|
68
|
+
try {
|
|
69
|
+
lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error.code === 'ENOENT') return [];
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
const account = String(options.account || '').trim();
|
|
75
|
+
const limit = Number.isInteger(options.limit) ? options.limit : 20;
|
|
76
|
+
return lines.reduce((rows, line) => {
|
|
77
|
+
try {
|
|
78
|
+
const row = JSON.parse(line);
|
|
79
|
+
if (!account || row.account === account) rows.push(row);
|
|
80
|
+
} catch {}
|
|
81
|
+
return rows;
|
|
82
|
+
}, []).reverse().slice(0, limit);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function printGmailVerdicts(options = {}) {
|
|
86
|
+
const rows = readGmailVerdicts(options);
|
|
87
|
+
if (!rows.length) {
|
|
88
|
+
console.log('no gmail verdicts found.');
|
|
89
|
+
return rows;
|
|
90
|
+
}
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
const details = [row.from && `from ${row.from}`, row.subject && `subject ${row.subject}`].filter(Boolean);
|
|
93
|
+
console.log(`${row.ts} archive ${row.message_id} account ${row.account}${details.length ? `, ${details.join(', ')}` : ''}`);
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseGmailVerdictsArgs(args = []) {
|
|
99
|
+
let account = null;
|
|
100
|
+
let limit = 20;
|
|
101
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
102
|
+
const arg = args[i];
|
|
103
|
+
const value = String(args[i + 1] || '').trim();
|
|
104
|
+
if (arg === '--account' && value && !value.startsWith('--')) account = value;
|
|
105
|
+
else if (arg === '--limit' && /^\d+$/.test(value) && Number(value) > 0) limit = Number(value);
|
|
106
|
+
else {
|
|
107
|
+
console.error(GMAIL_VERDICTS_USAGE);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
i += 1;
|
|
111
|
+
}
|
|
112
|
+
return { account, limit };
|
|
113
|
+
}
|
|
42
114
|
|
|
43
115
|
function gmailAccountStatePath() {
|
|
44
116
|
return process.env.ATRIS_GMAIL_ACCOUNT_FILE
|
|
@@ -312,6 +384,24 @@ async function gmailArchive(messageIds, options = {}) {
|
|
|
312
384
|
}
|
|
313
385
|
|
|
314
386
|
const archived = result.data?.archived ?? result.data?.count ?? ids.length;
|
|
387
|
+
const responseMessages = [
|
|
388
|
+
result.data?.messages,
|
|
389
|
+
result.data?.archived_messages,
|
|
390
|
+
result.data?.results,
|
|
391
|
+
].find(Array.isArray) || [];
|
|
392
|
+
const metadata = new Map(responseMessages.map((message) => [
|
|
393
|
+
String(message.id || message.message_id || ''),
|
|
394
|
+
message,
|
|
395
|
+
]));
|
|
396
|
+
appendGmailVerdicts(ids.map((messageId) => {
|
|
397
|
+
const message = metadata.get(messageId) || {};
|
|
398
|
+
return {
|
|
399
|
+
account: accountId,
|
|
400
|
+
message_id: messageId,
|
|
401
|
+
from: message.from || message.sender || undefined,
|
|
402
|
+
subject: message.subject || undefined,
|
|
403
|
+
};
|
|
404
|
+
}));
|
|
315
405
|
console.log(`Archived ${archived} message${archived === 1 ? '' : 's'}. They stay searchable in All Mail.`);
|
|
316
406
|
}
|
|
317
407
|
|
|
@@ -610,6 +700,11 @@ async function gmailCommand(subcommand, ...args) {
|
|
|
610
700
|
await gmailArchive(parsed.positional, { accountId: parsed.accountId || undefined });
|
|
611
701
|
break;
|
|
612
702
|
}
|
|
703
|
+
case 'verdicts': {
|
|
704
|
+
const parsed = parseGmailVerdictsArgs(args);
|
|
705
|
+
printGmailVerdicts(parsed);
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
613
708
|
case 'send': {
|
|
614
709
|
const parsed = parseGmailSendArgs(args);
|
|
615
710
|
await gmailSend(parsed.to, parsed.subject, parsed.body, { accountId: parsed.accountId || undefined });
|
|
@@ -634,6 +729,7 @@ async function gmailCommand(subcommand, ...args) {
|
|
|
634
729
|
console.log(' atris gmail inbox [--account <id>] - list recent emails for a mailbox');
|
|
635
730
|
console.log(' atris gmail read <id> [--account <id>] - read specific email');
|
|
636
731
|
console.log(' atris gmail archive <id> [...] [--account <id>] - archive messages (reversible, all mail keeps them)');
|
|
732
|
+
console.log(' atris gmail verdicts [--account <id>] [--limit N] - list recent archive verdicts');
|
|
637
733
|
console.log(' atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - send an email');
|
|
638
734
|
console.log(' atris gmail voice [account] [--clear] - edit or clear an account writing voice');
|
|
639
735
|
console.log(' atris gmail connect [name] - connect or reconnect a gmail account');
|
|
@@ -2153,6 +2249,10 @@ module.exports = {
|
|
|
2153
2249
|
extractGmailMailboxAccount,
|
|
2154
2250
|
parseGmailArgs,
|
|
2155
2251
|
gmailAccountStatePath,
|
|
2252
|
+
gmailVerdictsPath,
|
|
2253
|
+
appendGmailVerdicts,
|
|
2254
|
+
readGmailVerdicts,
|
|
2255
|
+
printGmailVerdicts,
|
|
2156
2256
|
calendarCommand,
|
|
2157
2257
|
twitterCommand,
|
|
2158
2258
|
slackCommand,
|