atris 3.73.1 → 3.74.1
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/bin/atris.js +85 -82
- package/commands/aeo.js +2 -2
- package/commands/align.js +10 -19
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +4 -8
- package/commands/avail.js +36 -7
- package/commands/computer.js +3 -1
- package/commands/console.js +1 -1
- package/commands/context-sync.js +1 -60
- package/commands/fleet.js +0 -1
- package/commands/lifecycle.js +17 -6
- package/commands/log-sync.js +1 -1
- package/commands/member.js +158 -18
- package/commands/mission.js +157 -69
- package/commands/now.js +0 -7
- package/commands/plugin.js +1 -1
- package/commands/pull.js +1 -1
- package/commands/push.js +0 -1
- package/commands/radar.js +0 -4
- package/commands/recap.js +25 -1
- package/commands/report.js +0 -4
- package/commands/review.js +1 -1
- package/commands/setup.js +6 -2
- package/commands/slop.js +8 -2
- package/commands/task.js +0 -522
- package/commands/terminal.js +1 -1
- package/commands/youtube.js +4 -6
- package/commands/zen.js +4 -4
- package/lib/agent-today.js +412 -0
- package/lib/autoland.js +0 -8
- package/lib/bench/runner.js +0 -4
- package/lib/chat-log-scan.js +0 -28
- package/lib/cli-scope.js +4 -2
- package/lib/engine-registry.js +0 -4
- package/lib/falsifier-probe.js +2 -1
- package/lib/file-ops.js +6 -1
- package/lib/first-minute.js +2 -0
- package/lib/gm-bet.js +125 -30
- package/lib/gm-phone-page.js +398 -99
- package/lib/gm-phone.js +30 -3
- package/lib/handoff-wake.js +180 -0
- package/lib/manifest.js +0 -10
- package/lib/member-switches.js +0 -11
- package/lib/member-up.js +18 -3
- package/lib/mission-protected-lane.js +38 -6
- package/lib/review-integrity.js +0 -4
- package/lib/spawn-env.js +92 -3
- package/lib/task-proof.js +2 -1
- package/lib/task-receipt.js +3 -2
- package/lib/yes-lock.js +633 -81
- package/package.json +1 -1
package/commands/align.js
CHANGED
|
@@ -42,15 +42,6 @@ function sleep(ms) {
|
|
|
42
42
|
return new Promise((r) => setTimeout(r, ms));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* Compute SHA-256 hash of content string.
|
|
47
|
-
* @param {string} content - Content to hash
|
|
48
|
-
* @returns {string} Hex-encoded hash
|
|
49
|
-
*/
|
|
50
|
-
function hashContent(content) {
|
|
51
|
-
return crypto.createHash('sha256').update(content).digest('hex');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
45
|
/**
|
|
55
46
|
* Compute SHA-256 hash of a file.
|
|
56
47
|
* @param {string} absPath - Absolute path to file
|
|
@@ -472,19 +463,19 @@ async function alignAtris() {
|
|
|
472
463
|
const printUsage = () => {
|
|
473
464
|
console.log('Usage: atris align [business] [--fix] [--hard] [--from cloud|local] [--dry-run]');
|
|
474
465
|
console.log('');
|
|
475
|
-
console.log(' atris align
|
|
476
|
-
console.log(' atris align example-co
|
|
477
|
-
console.log(' atris align example-co --fix
|
|
478
|
-
console.log(' atris align example-co --fix --hard
|
|
479
|
-
console.log(' atris align example-co --fix --from cloud
|
|
480
|
-
console.log(' atris align example-co --dry-run Show what would change,
|
|
466
|
+
console.log(' atris align Show what differs between this folder and its cloud copy');
|
|
467
|
+
console.log(' atris align example-co Show what differs for the example-co workspace');
|
|
468
|
+
console.log(' atris align example-co --fix Make them match, keeping this computer\'s version');
|
|
469
|
+
console.log(' atris align example-co --fix --hard Make the cloud an exact copy of this computer: delete cloud-only files, upload the rest. Fast.');
|
|
470
|
+
console.log(' atris align example-co --fix --from cloud Keep the cloud version: copy cloud-only files down, delete extra local files');
|
|
471
|
+
console.log(' atris align example-co --dry-run Show what would change, change nothing');
|
|
481
472
|
};
|
|
482
473
|
|
|
483
474
|
// Explicit help must win before we try to auto-detect a business slug,
|
|
484
475
|
// otherwise `--help` gets overwritten by the .atris/business.json fallback
|
|
485
476
|
// and the command attempts a real align instead of printing usage.
|
|
486
|
-
const
|
|
487
|
-
if (
|
|
477
|
+
const alignArgs = process.argv.slice(3);
|
|
478
|
+
if (alignArgs[0] === 'help' || alignArgs.includes('--help') || alignArgs.includes('-h')) {
|
|
488
479
|
printUsage();
|
|
489
480
|
process.exit(0);
|
|
490
481
|
}
|
|
@@ -515,7 +506,7 @@ async function alignAtris() {
|
|
|
515
506
|
}
|
|
516
507
|
|
|
517
508
|
if (hard && fromSide !== 'local') {
|
|
518
|
-
console.error('--hard only
|
|
509
|
+
console.error('--hard only works with --from local: it overwrites the cloud with this computer\'s files, never the other way.');
|
|
519
510
|
process.exit(1);
|
|
520
511
|
}
|
|
521
512
|
|
|
@@ -546,7 +537,7 @@ async function alignAtris() {
|
|
|
546
537
|
// Wake EC2 (the rule)
|
|
547
538
|
const endpoint = await ensureAwake(creds.token, biz.businessId);
|
|
548
539
|
if (!endpoint) {
|
|
549
|
-
console.error(
|
|
540
|
+
console.error(` The cloud computer for "${slug}" did not wake up in time, so nothing ran. Wait a minute, then run the same command again.`);
|
|
550
541
|
process.exit(1);
|
|
551
542
|
}
|
|
552
543
|
|
package/commands/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { AGENT_TOKEN_EXPIRED_DETAIL, decodeJwtClaims, loadCredentials, saveCredentials, saveAgentClientCredentials, preserveAgentClientLogin, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile,
|
|
1
|
+
const { AGENT_TOKEN_EXPIRED_DETAIL, decodeJwtClaims, loadCredentials, saveCredentials, saveAgentClientCredentials, preserveAgentClientLogin, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
|
|
2
2
|
const { getAppBaseUrl, getApiBaseUrl, apiRequestJson } = require('../utils/api');
|
|
3
3
|
const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
|
|
4
4
|
const { hasFlag, readFlag } = require('../lib/arg-parser');
|
package/commands/autopilot.js
CHANGED
|
@@ -1163,7 +1163,10 @@ function lessonSlug(text, maxLen = 40) {
|
|
|
1163
1163
|
*/
|
|
1164
1164
|
function writeLesson(cwd, slug, status, explanation) {
|
|
1165
1165
|
const lessonsPath = path.join(cwd, 'atris', 'lessons.md');
|
|
1166
|
-
|
|
1166
|
+
// Local calendar day (YYYY-MM-DD); toISOString is UTC and dates an
|
|
1167
|
+
// evening lesson tomorrow anywhere west of Greenwich.
|
|
1168
|
+
const now = new Date();
|
|
1169
|
+
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
1167
1170
|
const lessonLine = `- **[${today}] ${slug}** - ${status} - ${explanation}`;
|
|
1168
1171
|
|
|
1169
1172
|
if (!fs.existsSync(lessonsPath)) {
|
|
@@ -4043,13 +4046,6 @@ Search the codebase to verify. Reply: YES <reason> or NO <reason>`;
|
|
|
4043
4046
|
}
|
|
4044
4047
|
}
|
|
4045
4048
|
|
|
4046
|
-
/**
|
|
4047
|
-
* Entry point when called without a description.
|
|
4048
|
-
*/
|
|
4049
|
-
async function autopilotFromTodo(options = {}) {
|
|
4050
|
-
return autopilotAtris(null, options);
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
4049
|
module.exports = {
|
|
4054
4050
|
askModel,
|
|
4055
4051
|
snapshotRepoHeads,
|
package/commands/avail.js
CHANGED
|
@@ -117,6 +117,21 @@ function resolveTimezone({ tz, where }) {
|
|
|
117
117
|
return 'America/Los_Angeles';
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function parseWholeNumber(raw, min, max) {
|
|
121
|
+
if (raw === undefined || !/^\d+$/.test(String(raw).trim())) return null;
|
|
122
|
+
const n = parseInt(raw, 10);
|
|
123
|
+
if (n < min || n > max) return null;
|
|
124
|
+
return n;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function requireWholeNumber(flag, raw, min, max) {
|
|
128
|
+
const n = parseWholeNumber(raw, min, max);
|
|
129
|
+
if (n === null) {
|
|
130
|
+
throw new Error(`${flag} needs a whole number from ${min} to ${max}, got: ${raw === undefined ? 'nothing' : raw}`);
|
|
131
|
+
}
|
|
132
|
+
return n;
|
|
133
|
+
}
|
|
134
|
+
|
|
120
135
|
function parseSetArgs(argv) {
|
|
121
136
|
const opts = {
|
|
122
137
|
wake: null,
|
|
@@ -138,10 +153,10 @@ function parseSetArgs(argv) {
|
|
|
138
153
|
else if (a === '--tz') opts.tz = argv[++i];
|
|
139
154
|
else if (a === '--where') opts.where = argv[++i];
|
|
140
155
|
else if (a === '--days') opts.days = argv[++i];
|
|
141
|
-
else if (a === '--slot-minutes') opts.slotMinutes =
|
|
142
|
-
else if (a === '--window-days') opts.windowDays =
|
|
156
|
+
else if (a === '--slot-minutes') opts.slotMinutes = requireWholeNumber(a, argv[++i], 5, 480);
|
|
157
|
+
else if (a === '--window-days') opts.windowDays = requireWholeNumber(a, argv[++i], 1, 365);
|
|
143
158
|
else if (a === '--help' || a === '-h') return { help: true };
|
|
144
|
-
else throw new Error(`
|
|
159
|
+
else throw new Error(`atris avail set does not take "${a}". Run: atris avail --help`);
|
|
145
160
|
}
|
|
146
161
|
if (opts.wake == null || opts.sleep == null) {
|
|
147
162
|
throw new Error('--wake and --sleep are required (e.g. --wake 2:30pm --sleep 9pm)');
|
|
@@ -287,18 +302,30 @@ async function setAvail(argv) {
|
|
|
287
302
|
return 0;
|
|
288
303
|
}
|
|
289
304
|
|
|
305
|
+
function parseSlotsDays(raw) {
|
|
306
|
+
return parseWholeNumber(raw, 1, 365);
|
|
307
|
+
}
|
|
308
|
+
|
|
290
309
|
async function slotsAvail(argv) {
|
|
291
310
|
const opts = { days: 7, username: null, json: false };
|
|
292
311
|
for (let i = 0; i < argv.length; i += 1) {
|
|
293
312
|
const a = argv[i];
|
|
294
313
|
if (a === '--json') opts.json = true;
|
|
295
|
-
else if (a === '--days')
|
|
314
|
+
else if (a === '--days') {
|
|
315
|
+
const raw = argv[++i];
|
|
316
|
+
const days = parseSlotsDays(raw);
|
|
317
|
+
if (days === null) {
|
|
318
|
+
console.error(`--days needs a whole number from 1 to 365, got: ${raw === undefined ? 'nothing' : raw}`);
|
|
319
|
+
return 1;
|
|
320
|
+
}
|
|
321
|
+
opts.days = days;
|
|
322
|
+
}
|
|
296
323
|
else if (a === '--user' || a === '--username') opts.username = argv[++i];
|
|
297
324
|
else if (a === '--help' || a === '-h') {
|
|
298
325
|
printHelp();
|
|
299
326
|
return 0;
|
|
300
327
|
} else {
|
|
301
|
-
console.error(`
|
|
328
|
+
console.error(`atris avail slots does not take "${a}". Run: atris avail --help`);
|
|
302
329
|
return 1;
|
|
303
330
|
}
|
|
304
331
|
}
|
|
@@ -320,7 +347,7 @@ async function slotsAvail(argv) {
|
|
|
320
347
|
try {
|
|
321
348
|
data = JSON.parse(result.body.toString('utf8'));
|
|
322
349
|
} catch {
|
|
323
|
-
console.error(
|
|
350
|
+
console.error(`the booking server sent back a reply atris could not read (HTTP ${result.status}). Try again in a minute.`);
|
|
324
351
|
return 1;
|
|
325
352
|
}
|
|
326
353
|
if (result.status < 200 || result.status >= 300) {
|
|
@@ -392,7 +419,7 @@ async function availCommand(argv = []) {
|
|
|
392
419
|
const sub = argv[0].toLowerCase();
|
|
393
420
|
if (sub === 'set' || sub === 'update') return setAvail(argv.slice(1));
|
|
394
421
|
if (sub === 'slots') return slotsAvail(argv.slice(1));
|
|
395
|
-
console.error(`
|
|
422
|
+
console.error(`atris avail does not know "${sub}". Use show, set, or slots.`);
|
|
396
423
|
printHelp();
|
|
397
424
|
return 1;
|
|
398
425
|
}
|
|
@@ -402,6 +429,8 @@ module.exports = {
|
|
|
402
429
|
formatBookingLink,
|
|
403
430
|
parseTimeToDecimal,
|
|
404
431
|
parseDaysArg,
|
|
432
|
+
parseSlotsDays,
|
|
433
|
+
parseSetArgs,
|
|
405
434
|
buildAvailableHours,
|
|
406
435
|
resolveTimezone,
|
|
407
436
|
formatDecimalHour,
|
package/commands/computer.js
CHANGED
|
@@ -35,7 +35,7 @@ const {
|
|
|
35
35
|
runEngineDeviceLoginCommand,
|
|
36
36
|
validEngineLoginSeat,
|
|
37
37
|
} = require('./engine');
|
|
38
|
-
const { consoleCommand
|
|
38
|
+
const { consoleCommand } = require('./console');
|
|
39
39
|
const { streamSession } = require('./serve');
|
|
40
40
|
const { buildRemoteAtrisBootstrapCommand } = require('../lib/runtime-bootstrap');
|
|
41
41
|
const { envForcesNonInteractive } = require('../lib/noninteractive');
|
|
@@ -4380,6 +4380,7 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
|
|
|
4380
4380
|
if (!codeopsCtx) {
|
|
4381
4381
|
console.error('Atris CodeOps is not available for this account.');
|
|
4382
4382
|
console.error('Ask an Atris CodeOps admin to add you to the atris-codeops business.');
|
|
4383
|
+
process.exitCode = 1;
|
|
4383
4384
|
return;
|
|
4384
4385
|
}
|
|
4385
4386
|
const codeopsOptions = codeOpsCloudOptions(cloudOptions);
|
|
@@ -4418,6 +4419,7 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
|
|
|
4418
4419
|
default:
|
|
4419
4420
|
console.error(`Unknown CodeOps subcommand: ${codeopsSub}`);
|
|
4420
4421
|
console.log('Run: atris computer codeops help');
|
|
4422
|
+
process.exitCode = 1;
|
|
4421
4423
|
return;
|
|
4422
4424
|
}
|
|
4423
4425
|
return;
|
package/commands/console.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
|
-
const {
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const { resolveClaudeRunnerBin } = require('../lib/runner-command');
|
|
7
7
|
|
package/commands/context-sync.js
CHANGED
|
@@ -222,65 +222,6 @@ async function businessDiff(slug) {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
|
|
225
|
-
/**
|
|
226
|
-
* atris log <business-slug>
|
|
227
|
-
* Shows human-readable commit history.
|
|
228
|
-
*/
|
|
229
|
-
async function businessLog(slug) {
|
|
230
|
-
const biz = await resolveBusiness(slug);
|
|
231
|
-
|
|
232
|
-
if (!biz.workspaceId) {
|
|
233
|
-
console.error(`Business "${slug}" has no workspace.`);
|
|
234
|
-
process.exit(1);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const limit = 20;
|
|
238
|
-
const pathFilter = process.argv.includes('--path') ? process.argv[process.argv.indexOf('--path') + 1] : null;
|
|
239
|
-
|
|
240
|
-
const params = `limit=${limit}` + (pathFilter ? `&path=${encodeURIComponent(pathFilter)}` : '');
|
|
241
|
-
const result = await apiRequestJson(
|
|
242
|
-
`/business/${biz.businessId}/workspaces/${biz.workspaceId}/git/log?${params}`,
|
|
243
|
-
{ method: 'GET', token: biz.token }
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
if (!result.ok) {
|
|
247
|
-
if (result.status === 409) {
|
|
248
|
-
console.log('\n Computer is sleeping. Wake it first.\n');
|
|
249
|
-
} else {
|
|
250
|
-
console.error(`\n Failed to get history: ${result.errorMessage || result.status}\n`);
|
|
251
|
-
}
|
|
252
|
-
process.exit(1);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const commits = result.data.commits || [];
|
|
256
|
-
if (commits.length === 0) {
|
|
257
|
-
console.log(`\n ${biz.name}: no history yet.\n`);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
console.log(`\n ${biz.name}: history\n`);
|
|
262
|
-
|
|
263
|
-
for (const commit of commits) {
|
|
264
|
-
const date = _timeSince(commit.date) || commit.date;
|
|
265
|
-
const msg = commit.message || '';
|
|
266
|
-
|
|
267
|
-
// Parse actor from message format "actor/name: message"
|
|
268
|
-
const colonIdx = msg.indexOf(': ');
|
|
269
|
-
let actor = '';
|
|
270
|
-
let description = msg;
|
|
271
|
-
if (colonIdx > 0 && colonIdx < 40) {
|
|
272
|
-
actor = msg.substring(0, colonIdx);
|
|
273
|
-
description = msg.substring(colonIdx + 2);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const actorDisplay = actor ? ` ${actor}` : '';
|
|
277
|
-
console.log(` ${date.padEnd(12)} ${actorDisplay}`);
|
|
278
|
-
console.log(` ${description}`);
|
|
279
|
-
console.log('');
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
225
|
/**
|
|
285
226
|
* Convert ISO timestamp to human-readable relative time (e.g., "5m ago").
|
|
286
227
|
* @param {string} isoString - ISO 8601 timestamp
|
|
@@ -300,4 +241,4 @@ function _timeSince(isoString) {
|
|
|
300
241
|
}
|
|
301
242
|
|
|
302
243
|
|
|
303
|
-
module.exports = { businessStatus, businessDiff
|
|
244
|
+
module.exports = { businessStatus, businessDiff };
|
package/commands/fleet.js
CHANGED
|
@@ -18,7 +18,6 @@ const https = require('https');
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const os = require('os');
|
|
21
|
-
const crypto = require('crypto');
|
|
22
21
|
const { parseScopeFlag } = require('../lib/cli-scope');
|
|
23
22
|
|
|
24
23
|
// ── Config ──────────────────────────────────────────────
|
package/commands/lifecycle.js
CHANGED
|
@@ -11,6 +11,15 @@ const {
|
|
|
11
11
|
setMemberLoopAwake,
|
|
12
12
|
} = require('../lib/member-switches');
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* True when the user asked for help. Checked before anything else so
|
|
16
|
+
* `atris sleep --help` inside a bound folder never sleeps that business.
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
function wantsHelp() {
|
|
20
|
+
return process.argv.slice(3).some((a) => a === '--help' || a === '-h' || a === 'help');
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Resolve business slug from CLI arg or local .atris/business.json.
|
|
16
25
|
* @returns {string|null} The resolved slug, or null if not found.
|
|
@@ -121,11 +130,12 @@ async function waitForBusinessComputer(token, business, maxWait = 90000) {
|
|
|
121
130
|
* @returns {Promise<void>}
|
|
122
131
|
*/
|
|
123
132
|
async function sleepAtris() {
|
|
124
|
-
|
|
133
|
+
const help = wantsHelp();
|
|
134
|
+
if (!help && handleMemberSwitch(false)) return;
|
|
125
135
|
|
|
126
|
-
const slug = resolveSlug();
|
|
136
|
+
const slug = help ? null : resolveSlug();
|
|
127
137
|
|
|
128
|
-
if (!slug
|
|
138
|
+
if (!slug) {
|
|
129
139
|
console.log('Usage: atris sleep [business|member] [--loop <loop-id>]');
|
|
130
140
|
console.log('');
|
|
131
141
|
console.log(' Pause a workspace to save compute. Storage only.');
|
|
@@ -172,11 +182,12 @@ async function sleepAtris() {
|
|
|
172
182
|
* @returns {Promise<void>}
|
|
173
183
|
*/
|
|
174
184
|
async function wakeAtris() {
|
|
175
|
-
|
|
185
|
+
const help = wantsHelp();
|
|
186
|
+
if (!help && handleMemberSwitch(true)) return;
|
|
176
187
|
|
|
177
|
-
const slug = resolveSlug();
|
|
188
|
+
const slug = help ? null : resolveSlug();
|
|
178
189
|
|
|
179
|
-
if (!slug
|
|
190
|
+
if (!slug) {
|
|
180
191
|
console.log('Usage: atris wake [business|member] [--loop <loop-id>]');
|
|
181
192
|
console.log('');
|
|
182
193
|
console.log(' Wake a sleeping workspace. Wake signal sent; loops pick it up on their next pass.');
|
package/commands/log-sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { getLogPath,
|
|
3
|
+
const { getLogPath, createLogFile } = require('../lib/file-ops');
|
|
4
4
|
const { parseJournalSections, mergeSections, reconstructJournal, showLogDiff } = require('../lib/journal');
|
|
5
5
|
const { computeContentHash, isSameTimestamp } = require('../lib/journal');
|
|
6
6
|
const { loadConfig, loadLogSyncState, saveLogSyncState } = require('../utils/config');
|
package/commands/member.js
CHANGED
|
@@ -777,6 +777,8 @@ const MEMBER_RUN_START_VALUE_FLAGS = [
|
|
|
777
777
|
'--truth',
|
|
778
778
|
'--proof',
|
|
779
779
|
'--base',
|
|
780
|
+
'--yes-lock',
|
|
781
|
+
'--yes-lock-parent',
|
|
780
782
|
];
|
|
781
783
|
|
|
782
784
|
const MEMBER_RUN_START_BOOLEAN_FLAGS = [
|
|
@@ -1000,6 +1002,12 @@ function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cw
|
|
|
1000
1002
|
pushFlagValue(startArgs, args, '--minutes');
|
|
1001
1003
|
pushFlagValue(startArgs, args, '--hours');
|
|
1002
1004
|
pushFlagValue(startArgs, args, '--base');
|
|
1005
|
+
// a run inside a woken member's run keeps its seal: mission start reads it
|
|
1006
|
+
// from the environment, never from these words
|
|
1007
|
+
if (!process.env.ATRIS_HANDOFF_YES_LOCK) {
|
|
1008
|
+
pushFlagValue(startArgs, args, '--yes-lock');
|
|
1009
|
+
pushFlagValue(startArgs, args, '--yes-lock-parent');
|
|
1010
|
+
}
|
|
1003
1011
|
// Member runs choose and drive work immediately, so an absent check must not
|
|
1004
1012
|
// leave autonomous callers with a mission that never ran. A check inferred
|
|
1005
1013
|
// from the mission text counts as named.
|
|
@@ -1297,6 +1305,10 @@ function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
|
|
|
1297
1305
|
console.error(`${name} is parked; running anyway`);
|
|
1298
1306
|
}
|
|
1299
1307
|
} catch { /* an unreadable card is requireMemberDir's problem, not this notice's */ }
|
|
1308
|
+
if (args.filter(require('./mission').isYesLockWord).length > 1) {
|
|
1309
|
+
return printMemberRunResult({ started: false, state: null, mission_id: null,
|
|
1310
|
+
error: 'member run got more than one --yes-lock, so it is not clear which seal applies; nothing started' }, asJson, { quiet });
|
|
1311
|
+
}
|
|
1300
1312
|
const requestedVerifier = readFlag(args, '--verify', '') || memberRunConfiguredVerifier(paths);
|
|
1301
1313
|
const missionText = memberRunMissionText(args);
|
|
1302
1314
|
const hasMissionOverride = Boolean(readFlag(args, '--mission', '') || readFlag(args, '--mission-id', ''));
|
|
@@ -1362,7 +1374,7 @@ function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
|
|
|
1362
1374
|
const missionWordIndices = missionText && !hasMissionOverride ? new Set(memberRunMissionWordIndices(args)) : new Set();
|
|
1363
1375
|
const executionArgs = args.filter((_, index) => !missionWordIndices.has(index));
|
|
1364
1376
|
const runArgs = stripKnownFlags(executionArgs,
|
|
1365
|
-
['--mission', '--mission-id', '--minutes', '--hours', '--industry', '--domain', '--value', '--outcome', '--truth', '--proof', '--base'],
|
|
1377
|
+
['--mission', '--mission-id', '--minutes', '--hours', '--industry', '--domain', '--value', '--outcome', '--truth', '--proof', '--base', '--yes-lock', '--yes-lock-parent'],
|
|
1366
1378
|
['--worktree', '--shared-checkout', '--no-worktree', '--choose-work', '--auto', '--ignite', '--wait', '--sync']);
|
|
1367
1379
|
if (ignite && !hasFlag(runArgs, '--complete-on-pass')) runArgs.push('--complete-on-pass');
|
|
1368
1380
|
if (memberRunWantsReferee(runArgs, ignite)) runArgs.push('--referee');
|
|
@@ -1374,6 +1386,16 @@ function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
|
|
|
1374
1386
|
const caller = callerRuntime(args);
|
|
1375
1387
|
const runnerSwap = memberRunRunnerSwap(mission, args, caller);
|
|
1376
1388
|
if (runnerSwap) runArgs.push('--engine', runnerSwap.to);
|
|
1389
|
+
// A run in the foreground (wake, go, run by name) is noted on the start
|
|
1390
|
+
// list under the shared lock, so member up and handoffs count it while it
|
|
1391
|
+
// works. A detached run is noted by whoever started it.
|
|
1392
|
+
if (!hasFlag(runArgs, '--detach') && !hasFlag(runArgs, '--dry-run')) {
|
|
1393
|
+
try {
|
|
1394
|
+
const stateDir = path.join(require('./mission').resolveMemberWorkspaceRoot(process.cwd()), '.atris', 'state');
|
|
1395
|
+
require('../lib/handoff-wake').withWakeReservation(stateDir,
|
|
1396
|
+
({ record }) => record({ member: name, mission_id: mission.id, pid: process.pid }), { waitMs: 30000 });
|
|
1397
|
+
} catch {}
|
|
1398
|
+
}
|
|
1377
1399
|
const run = memberRunCli(['mission', 'run', mission.id, ...runArgs], cwd, { live: !asJson && !quiet, missionId: mission.id });
|
|
1378
1400
|
const payload = run.payload || {};
|
|
1379
1401
|
const finalMission = payload.mission || require('./mission').resolveMission(mission.id, cwd) || mission;
|
|
@@ -4491,13 +4513,15 @@ function memberList(...flags) {
|
|
|
4491
4513
|
|
|
4492
4514
|
console.log('');
|
|
4493
4515
|
console.log('Team Members');
|
|
4494
|
-
console.log('─'.repeat(
|
|
4516
|
+
console.log('─'.repeat(72));
|
|
4495
4517
|
|
|
4496
4518
|
const nameW = 16;
|
|
4497
|
-
const roleW =
|
|
4498
|
-
const fmtW =
|
|
4519
|
+
const roleW = 18;
|
|
4520
|
+
const fmtW = 8;
|
|
4499
4521
|
const skillW = 8;
|
|
4500
|
-
const ctxW =
|
|
4522
|
+
const ctxW = 9;
|
|
4523
|
+
// Keep one space between columns, even when a value fills its column.
|
|
4524
|
+
const cell = (value, width) => `${clipCell(value, width - 1)} `;
|
|
4501
4525
|
|
|
4502
4526
|
console.log(
|
|
4503
4527
|
' ' +
|
|
@@ -4508,18 +4532,18 @@ function memberList(...flags) {
|
|
|
4508
4532
|
'Context'.padEnd(ctxW) +
|
|
4509
4533
|
'Version'
|
|
4510
4534
|
);
|
|
4511
|
-
console.log(' ' + '─'.repeat(
|
|
4535
|
+
console.log(' ' + '─'.repeat(70));
|
|
4512
4536
|
|
|
4513
4537
|
for (const m of members) {
|
|
4514
4538
|
const skills = m.format === 'directory' ? String(m.skillCount) : '-';
|
|
4515
4539
|
const context = m.format === 'directory' ? String(m.contextCount) : '-';
|
|
4516
4540
|
console.log(
|
|
4517
4541
|
' ' +
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4542
|
+
cell(m.name, nameW) +
|
|
4543
|
+
cell(m.role, roleW) +
|
|
4544
|
+
cell(m.format === 'directory' ? 'dir' : 'flat', fmtW) +
|
|
4545
|
+
cell(skills, skillW) +
|
|
4546
|
+
cell(context, ctxW) +
|
|
4523
4547
|
(m.version || '-') +
|
|
4524
4548
|
(parked.has(m.name) ? ' parked' : '')
|
|
4525
4549
|
);
|
|
@@ -9865,7 +9889,11 @@ async function memberImprovements(businessId, ...args) {
|
|
|
9865
9889
|
console.error('Not logged in. Run: atris login');
|
|
9866
9890
|
process.exit(1);
|
|
9867
9891
|
}
|
|
9868
|
-
|
|
9892
|
+
if (flags.days !== null && !(Number.isInteger(flags.days) && flags.days >= 1)) {
|
|
9893
|
+
console.error('--days needs a whole number of days, 1 or more (example: --days 7).');
|
|
9894
|
+
process.exit(1);
|
|
9895
|
+
}
|
|
9896
|
+
const days = flags.days || 30;
|
|
9869
9897
|
const result = await apiRequestJson(`/business/${encodeURIComponent(businessId)}/improvements/daily?days=${days}`, { token: creds.token });
|
|
9870
9898
|
if (!result.ok) {
|
|
9871
9899
|
console.error(`Failed to fetch improvements: ${result.error || 'Unknown error'}`);
|
|
@@ -11134,6 +11162,8 @@ async function memberUp(...args) {
|
|
|
11134
11162
|
pickTask: (name, taken) => memberUpPick(root, name, taken),
|
|
11135
11163
|
writeNextJob: (name, opts) => memberUpWriteNextJob(root, name, opts),
|
|
11136
11164
|
spawn: (name, onExit, onClaim) => memberUpSpawn(root, name, onExit, onClaim),
|
|
11165
|
+
// the start lock handoffs share, so both count what the other started
|
|
11166
|
+
reserve: (fn) => require('../lib/handoff-wake').withWakeReservation(path.join(root, '.atris', 'state'), fn),
|
|
11137
11167
|
now: () => Date.now(),
|
|
11138
11168
|
jobOf: (name) => memberGoJob(name, root),
|
|
11139
11169
|
waitingTasks: () => memberUpWaiting(root),
|
|
@@ -11352,10 +11382,108 @@ function collectMemberOneOnOnes(root = process.cwd(), slugs = []) {
|
|
|
11352
11382
|
return queue;
|
|
11353
11383
|
}
|
|
11354
11384
|
|
|
11385
|
+
// The env a woken member starts with: its own plain-terminal run, not part of
|
|
11386
|
+
// the session or mission driver that handed off (those markers would turn the
|
|
11387
|
+
// run into proof-only mode, a native-goal wait, or a child of the old driver).
|
|
11388
|
+
const HANDOFF_WAKE_SCRUB_ENV = [
|
|
11389
|
+
'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT', 'CODEX_SANDBOX', 'CURSOR_AGENT', 'DEVIN_SESSION_ID',
|
|
11390
|
+
'CODEX_THREAD_ID', 'CODEX_SESSION_ID', 'ATRIS_AGENT_ID',
|
|
11391
|
+
'ATRIS_MISSION_DRIVER_DETACHED', 'ATRIS_MISSION_DRIVER_PARENT_PID',
|
|
11392
|
+
];
|
|
11393
|
+
|
|
11394
|
+
// Start the receiver of a handoff on the handed-off step: the same bounded
|
|
11395
|
+
// work step `atris member wake <name>` runs (member run, 15 minutes), but
|
|
11396
|
+
// detached, so the member handing off is not held while the next one works.
|
|
11397
|
+
function startHandoffReceiver(root, to, step, parentMissionId = null) {
|
|
11398
|
+
const env = { ...process.env, ATRIS_CALLER_RUNTIME: 'terminal' };
|
|
11399
|
+
for (const name of HANDOFF_WAKE_SCRUB_ENV) delete env[name];
|
|
11400
|
+
// The handing-off mission's git guard (a temp folder of wrappers first on
|
|
11401
|
+
// PATH) belongs to that mission; the woken run sets up its own.
|
|
11402
|
+
env.PATH = String(env.PATH || '').split(path.delimiter)
|
|
11403
|
+
.filter((dir) => !path.basename(dir).startsWith(require('../lib/mission-protected-lane').GUARD_DIR_PREFIX))
|
|
11404
|
+
.join(path.delimiter);
|
|
11405
|
+
// Every woken run is under the fixed handoff seal, whatever handed off:
|
|
11406
|
+
// outward commands and merging to master wait for the owner's yes. The
|
|
11407
|
+
// parent only says where its yes cards show.
|
|
11408
|
+
// The seal goes by environment only, never on the command line the step
|
|
11409
|
+
// text shares, and every job the woken run starts inherits it. A step that
|
|
11410
|
+
// starts with "-" is quoted so it can never read as an option.
|
|
11411
|
+
env.ATRIS_HANDOFF_YES_LOCK = JSON.stringify(require('../lib/yes-lock').handoffSeal());
|
|
11412
|
+
if (parentMissionId) env.ATRIS_HANDOFF_YES_LOCK_PARENT = parentMissionId;
|
|
11413
|
+
else delete env.ATRIS_HANDOFF_YES_LOCK_PARENT;
|
|
11414
|
+
const stepText = /^\s*-/.test(String(step)) ? `step: ${String(step).trim()}` : String(step);
|
|
11415
|
+
const res = require('child_process').spawnSync(process.execPath, [
|
|
11416
|
+
path.join(__dirname, '..', 'bin', 'atris.js'),
|
|
11417
|
+
'member', 'run', to, stepText, '--minutes', '15', '--no-verify', '--detach', '--json',
|
|
11418
|
+
], { cwd: root, env, encoding: 'utf8', timeout: 120000 });
|
|
11419
|
+
let payload = null;
|
|
11420
|
+
const lines = String(res.stdout || '').trim().split('\n');
|
|
11421
|
+
for (let i = lines.length - 1; i >= 0 && !payload; i -= 1) {
|
|
11422
|
+
try { payload = JSON.parse(lines.slice(i).join('\n')); } catch {}
|
|
11423
|
+
}
|
|
11424
|
+
const ok = res.status === 0 && Boolean(payload && payload.ok !== false && payload.mission_id);
|
|
11425
|
+
return {
|
|
11426
|
+
ok,
|
|
11427
|
+
mission_id: payload ? payload.mission_id || null : null,
|
|
11428
|
+
pid: payload ? payload.pid || null : null,
|
|
11429
|
+
worktree: payload && payload.worktree ? payload.worktree.path || null : null,
|
|
11430
|
+
error: ok ? null : String((payload && payload.error) || res.stderr || (res.error && res.error.message) || `member run exited ${res.status}`),
|
|
11431
|
+
};
|
|
11432
|
+
}
|
|
11433
|
+
|
|
11434
|
+
// Whether to wake the receiver, and the outcome. Under the test runner a
|
|
11435
|
+
// handoff only records unless ATRIS_HANDOFF_WAKE=1 asks, so suites that hand
|
|
11436
|
+
// off never start a real engine by accident.
|
|
11437
|
+
function wakeOnHandoff(handoff, root, args) {
|
|
11438
|
+
const testRun = Boolean(process.env.NODE_TEST_CONTEXT) && process.env.ATRIS_HANDOFF_WAKE !== '1';
|
|
11439
|
+
if (hasFlag(args, '--no-wake') || testRun) {
|
|
11440
|
+
return {
|
|
11441
|
+
status: 'not_started',
|
|
11442
|
+
reason: 'not_asked',
|
|
11443
|
+
text: `${handoff.to} was not started; the handoff is only written down. to start it: atris member wake ${handoff.to}`,
|
|
11444
|
+
};
|
|
11445
|
+
}
|
|
11446
|
+
const { wakeHandoffReceiver, withWakeReservation } = require('../lib/handoff-wake');
|
|
11447
|
+
const parent = handoffParentMission(handoff, root);
|
|
11448
|
+
const outcome = wakeHandoffReceiver(handoff, {
|
|
11449
|
+
listMembers: () => memberUpTeam(root),
|
|
11450
|
+
awake: (name) => {
|
|
11451
|
+
try { return require('../lib/member-switches').isMemberAwake(name, root); } catch { return true; }
|
|
11452
|
+
},
|
|
11453
|
+
engine: (name) => require('../lib/member-engine').memberRosterEngine(name, root),
|
|
11454
|
+
start: (name, step) => startHandoffReceiver(root, name, step, parent),
|
|
11455
|
+
reserve: (fn) => withWakeReservation(path.join(root, '.atris', 'state'), fn),
|
|
11456
|
+
});
|
|
11457
|
+
return outcome.status === 'started' ? { ...outcome, yes_lock: 'handoff', yes_lock_parent: parent } : outcome;
|
|
11458
|
+
}
|
|
11459
|
+
|
|
11460
|
+
// The job a handoff came from, only to show the woken run's yes cards on it:
|
|
11461
|
+
// the mission whose guard this command runs under (a handoff from inside a
|
|
11462
|
+
// mission tick), else the job a [bet-...] tag in the step names. null when
|
|
11463
|
+
// neither is found; what the woken run holds never depends on it.
|
|
11464
|
+
function handoffParentMission(handoff, root) {
|
|
11465
|
+
const fromGuard = require('../lib/mission-protected-lane').guardMissionOnPath(process.env.PATH);
|
|
11466
|
+
if (fromGuard) return fromGuard;
|
|
11467
|
+
const bets = Array.from(String(handoff.summary || '').matchAll(/\[(bet-[A-Za-z0-9._-]+)\]/g)).map((m) => m[1]);
|
|
11468
|
+
if (!bets.length) return null;
|
|
11469
|
+
const dir = path.join(require('../lib/state-home').atrisHome(), 'state', 'yes-locks');
|
|
11470
|
+
let files = [];
|
|
11471
|
+
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')); } catch {}
|
|
11472
|
+
for (const name of files) {
|
|
11473
|
+
try {
|
|
11474
|
+
const data = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
11475
|
+
if (data && data.mission_id && bets.includes(data.bet_id)) return String(data.mission_id);
|
|
11476
|
+
} catch {}
|
|
11477
|
+
}
|
|
11478
|
+
return null;
|
|
11479
|
+
}
|
|
11480
|
+
|
|
11355
11481
|
function memberHandoff(from, to, ...args) {
|
|
11356
11482
|
if (from === '--help' || from === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
11357
|
-
console.log('Usage: atris member handoff [<from> <to> "<slice>"] [--json]');
|
|
11483
|
+
console.log('Usage: atris member handoff [<from> <to> "<slice>"] [--no-wake] [--json]');
|
|
11358
11484
|
console.log('Passes a completed slice from one member to the next specialist and queues their next_push.');
|
|
11485
|
+
console.log('The next member is started on the slice when it is not already working, unless the team is at its limit.');
|
|
11486
|
+
console.log('--no-wake only writes the handoff down.');
|
|
11359
11487
|
return;
|
|
11360
11488
|
}
|
|
11361
11489
|
const root = process.cwd();
|
|
@@ -11374,17 +11502,21 @@ function memberHandoff(from, to, ...args) {
|
|
|
11374
11502
|
}
|
|
11375
11503
|
const targetTo = sliceWords[0];
|
|
11376
11504
|
const summary = sliceWords.slice(1).join(' ').trim();
|
|
11377
|
-
const { recordMemberHandoff } = require('./mission');
|
|
11378
|
-
const
|
|
11379
|
-
if (!
|
|
11505
|
+
const { recordMemberHandoff, updateMemberHandoff, resolveMemberWorkspaceRoot } = require('./mission');
|
|
11506
|
+
const recorded = recordMemberHandoff(from, targetTo, summary, root);
|
|
11507
|
+
if (!recorded) {
|
|
11380
11508
|
console.error(`Could not record handoff from ${from} to ${targetTo}. Check member names.`);
|
|
11381
11509
|
process.exitCode = 1;
|
|
11382
11510
|
return null;
|
|
11383
11511
|
}
|
|
11512
|
+
const parentRoot = resolveMemberWorkspaceRoot(root);
|
|
11513
|
+
// the stored summary is cut at 180 characters; the wake gets the whole step
|
|
11514
|
+
const wake = { ...wakeOnHandoff({ ...recorded, summary }, parentRoot, args), at: new Date().toISOString() };
|
|
11515
|
+
const handoff = updateMemberHandoff(recorded.id, { wake }, parentRoot) || { ...recorded, wake };
|
|
11384
11516
|
const lines = [
|
|
11385
11517
|
`Handoff recorded: ${handoff.from} -> ${handoff.to}`,
|
|
11386
11518
|
`Slice: ${handoff.summary}`,
|
|
11387
|
-
|
|
11519
|
+
wake.text,
|
|
11388
11520
|
];
|
|
11389
11521
|
printJsonOrText({ ok: true, action: 'member_handoff', handoff }, lines, asJson);
|
|
11390
11522
|
return handoff;
|
|
@@ -11773,10 +11905,18 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11773
11905
|
// more often than a job. Starting a paid run on it costs money and
|
|
11774
11906
|
// does nothing useful, so name the miss and stop. Jobs keep working
|
|
11775
11907
|
// as more than one word, or through `atris member run "<job>"`.
|
|
11776
|
-
const
|
|
11908
|
+
const extraWordList = args.filter((a) => !String(a).startsWith('-'));
|
|
11909
|
+
const extraWords = extraWordList.length;
|
|
11777
11910
|
if (!/\s/.test(subcommand.trim()) && extraWords === 0) {
|
|
11778
11911
|
return memberUnknownWord(subcommand);
|
|
11779
11912
|
}
|
|
11913
|
+
// `atris member inbox validator`: an unknown word followed by a member
|
|
11914
|
+
// name is a command shape (verb, then who), not a job. It once started
|
|
11915
|
+
// a mission called "inbox validator"; name the miss instead.
|
|
11916
|
+
if (!/\s/.test(subcommand.trim()) && extraWords === 1
|
|
11917
|
+
&& isKnownMemberSlug(String(extraWordList[0]).trim(), process.cwd())) {
|
|
11918
|
+
return memberUnknownWord(subcommand);
|
|
11919
|
+
}
|
|
11780
11920
|
return memberSteerOrDispatch(subcommand, ...args);
|
|
11781
11921
|
}
|
|
11782
11922
|
console.log('run "<job>" pick the right member and run it: atris member run "fix the flaky login test"');
|