atris 3.65.0 → 3.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/atris/skills/engines/SKILL.md +2 -2
- package/bin/atris.js +41 -3
- package/commands/agent-client.js +266 -0
- package/commands/auth.js +225 -21
- package/commands/autoland.js +62 -0
- package/commands/brain.js +4 -0
- package/commands/engine.js +23 -1
- package/commands/link.js +367 -0
- package/commands/loops.js +4 -1
- package/commands/member.js +8 -1
- package/commands/orb.js +1 -1
- package/commands/xp.js +2 -0
- package/commands/youtube.js +1 -1
- package/lib/auto-accept-certified.js +89 -9
- package/lib/cleanup-summary.js +129 -0
- package/lib/engine-ask.js +3 -2
- package/lib/engine-registry.js +203 -15
- package/lib/idle-review-sweep.js +130 -0
- package/lib/known-commands.js +2 -2
- package/lib/land-green.js +18 -4
- package/lib/member-result.js +303 -0
- package/lib/roster-models.js +1 -1
- package/lib/runner-command.js +4 -4
- package/lib/task-list-keeper.js +25 -4
- package/lib/trust-tiers.js +131 -14
- package/lib/verify-worktree-rewrite.js +115 -0
- package/lib/voice-gate.js +2 -0
- package/package.json +1 -1
- package/scripts/det/ytnotes +1 -1
- package/utils/agent-client.js +363 -0
- package/utils/api.js +20 -2
- package/utils/auth.js +88 -3
package/commands/auth.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
const { AGENT_TOKEN_EXPIRED_DETAIL, decodeJwtClaims, loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
|
|
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, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
|
|
2
2
|
const { getAppBaseUrl, apiRequestJson } = require('../utils/api');
|
|
3
3
|
const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
|
|
4
4
|
const { hasFlag, readFlag } = require('../lib/arg-parser');
|
|
5
|
+
const agentClient = require('../utils/agent-client');
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const path = require('path');
|
|
7
8
|
|
|
@@ -128,7 +129,8 @@ function isAgentAccessToken(token) {
|
|
|
128
129
|
function scopedTokenCandidate(credentials = {}) {
|
|
129
130
|
if (credentials.agent_token) return credentials.agent_token;
|
|
130
131
|
if (
|
|
131
|
-
credentials.source ===
|
|
132
|
+
credentials.source === agentClient.AUTH_TYPE
|
|
133
|
+
|| credentials.source === 'env'
|
|
132
134
|
|| credentials.source === 'agent_token_file'
|
|
133
135
|
|| isAgentAccessToken(credentials.token)
|
|
134
136
|
) {
|
|
@@ -290,11 +292,16 @@ async function ensureBilledCommandAuth(scope, deps = {}) {
|
|
|
290
292
|
const scopes = claims?.scopes || credentials.agent_token_scopes || credentials.scopes || [];
|
|
291
293
|
const expiry = claims?.exp
|
|
292
294
|
? claims.exp * 1000
|
|
293
|
-
: Date.parse(credentials.agent_token_expires_at || credentials.expires_at);
|
|
295
|
+
: Date.parse(credentials.agent_token_expires_at || credentials.expires_at || credentials.token_expires_at);
|
|
294
296
|
if (!deps.forceMint && candidate && Array.isArray(scopes) && scopes.includes(wanted) && Number.isFinite(expiry) && expiry > Date.now()) {
|
|
295
297
|
return { ok: true, token: candidate, minted: false, credentials };
|
|
296
298
|
}
|
|
297
299
|
|
|
300
|
+
if (credentials.source === agentClient.AUTH_TYPE) {
|
|
301
|
+
// An agent client cannot mint itself more scope; the owner grants it.
|
|
302
|
+
return { ok: false, error: `this agent client does not have the ${wanted} scope. ask its owner to create one that does.` };
|
|
303
|
+
}
|
|
304
|
+
|
|
298
305
|
if (!canMintFromLogin(credentials)) {
|
|
299
306
|
return { ok: false, error: NO_STORED_JWT_MESSAGE };
|
|
300
307
|
}
|
|
@@ -382,6 +389,26 @@ async function mintAgentToken(args = [], deps = {}) {
|
|
|
382
389
|
|
|
383
390
|
async function printWhoamiPayload(asJson) {
|
|
384
391
|
const ensured = await ensureValidCredentials(apiRequestJson);
|
|
392
|
+
if (!ensured.error && ensured.source === 'agent_client') {
|
|
393
|
+
const creds = ensured.credentials || {};
|
|
394
|
+
const payload = {
|
|
395
|
+
ok: true,
|
|
396
|
+
logged_in: true,
|
|
397
|
+
auth_type: agentClient.AUTH_TYPE,
|
|
398
|
+
client_id: creds.client_id,
|
|
399
|
+
scopes: creds.scopes || [],
|
|
400
|
+
token_expires_at: creds.token_expires_at || null,
|
|
401
|
+
};
|
|
402
|
+
if (asJson) {
|
|
403
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
404
|
+
} else {
|
|
405
|
+
console.log('Status: Logged in ✓ (agent client)');
|
|
406
|
+
console.log(`Client ID: ${payload.client_id}`);
|
|
407
|
+
console.log(`Scopes: ${payload.scopes.length ? payload.scopes.join(', ') : 'unknown'}`);
|
|
408
|
+
if (payload.token_expires_at) console.log(`Token renews before: ${payload.token_expires_at}`);
|
|
409
|
+
}
|
|
410
|
+
process.exit(0);
|
|
411
|
+
}
|
|
385
412
|
if (ensured.error) {
|
|
386
413
|
if (!asJson && ensured.detail === AGENT_TOKEN_EXPIRED_DETAIL) {
|
|
387
414
|
console.error('the agent key expired.');
|
|
@@ -430,6 +457,144 @@ async function printWhoamiPayload(asJson) {
|
|
|
430
457
|
process.exit(0);
|
|
431
458
|
}
|
|
432
459
|
|
|
460
|
+
function flagPresent(args, name) {
|
|
461
|
+
return args.some((arg) => arg === name || String(arg).startsWith(`${name}=`));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function envValue(env, name) {
|
|
465
|
+
return typeof env[name] === 'string' ? env[name].trim() : '';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Decide whether this `atris login` is an agent client sign-in. Returns null
|
|
469
|
+
// for every other login path so those stay exactly as they were.
|
|
470
|
+
function agentClientLoginRequest(args = [], env = process.env) {
|
|
471
|
+
if (flagPresent(args, '--client-secret')) {
|
|
472
|
+
return { error: 'do not put the secret on the command line. use --client-secret-file <path> or ATRIS_CLIENT_SECRET.' };
|
|
473
|
+
}
|
|
474
|
+
const usesFlags = flagPresent(args, '--client-id') || flagPresent(args, '--client-secret-file');
|
|
475
|
+
const envId = envValue(env, 'ATRIS_CLIENT_ID');
|
|
476
|
+
if (!usesFlags && (!envId || flagPresent(args, '--token') || wantsAgentToken(args))) return null;
|
|
477
|
+
const clientId = readFlag(args, '--client-id', '') || envId;
|
|
478
|
+
if (!clientId) return { error: '--client-id needs a client id (atc_...)' };
|
|
479
|
+
if (!clientId.startsWith(agentClient.CLIENT_ID_PREFIX)) {
|
|
480
|
+
return { error: `client ids start with ${agentClient.CLIENT_ID_PREFIX}` };
|
|
481
|
+
}
|
|
482
|
+
const flagFile = readFlag(args, '--client-secret-file', '');
|
|
483
|
+
if (flagPresent(args, '--client-secret-file') && !flagFile) {
|
|
484
|
+
return { error: '--client-secret-file needs a path' };
|
|
485
|
+
}
|
|
486
|
+
const secretFile = flagFile || envValue(env, 'ATRIS_CLIENT_SECRET_FILE');
|
|
487
|
+
if (!secretFile && !envValue(env, 'ATRIS_CLIENT_SECRET')) {
|
|
488
|
+
return { error: 'no client secret. pass --client-secret-file <path>, or set ATRIS_CLIENT_SECRET_FILE or ATRIS_CLIENT_SECRET.' };
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
clientId,
|
|
492
|
+
secretFile: secretFile ? path.resolve(secretFile) : null,
|
|
493
|
+
scope: agentClient.scopeList(readFlag(args, '--scope', '') || readFlag(args, '--scopes', '')).join(' ') || null,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Keep the person's own login reachable with `atris switch` after the agent
|
|
498
|
+
// client takes over credentials.json.
|
|
499
|
+
function keepHumanLoginAsProfile(nextClientId) {
|
|
500
|
+
try {
|
|
501
|
+
const stored = JSON.parse(fs.readFileSync(getCredentialsPath(), 'utf8'));
|
|
502
|
+
if (agentClient.isAgentClientCredential(stored)) {
|
|
503
|
+
// A different agent client was signed in: keep it switchable too.
|
|
504
|
+
return stored.client_id === nextClientId ? null : preserveAgentClientLogin();
|
|
505
|
+
}
|
|
506
|
+
if (!stored || !stored.token) return null;
|
|
507
|
+
const name = profileNameFromEmail(stored.email);
|
|
508
|
+
if (!name) return null;
|
|
509
|
+
if (!loadProfile(name)) saveProfile(name, stored);
|
|
510
|
+
return name;
|
|
511
|
+
} catch {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function loginAgentClient(request, deps = {}) {
|
|
517
|
+
const env = deps.env || process.env;
|
|
518
|
+
const json = Boolean(deps.json);
|
|
519
|
+
const output = deps.output || console.log;
|
|
520
|
+
const outputError = deps.outputError || console.error;
|
|
521
|
+
const fail = (message, code) => {
|
|
522
|
+
if (json) output(JSON.stringify({ ok: false, logged_in: false, error: code || 'agent_client_login_failed', detail: message }, null, 2));
|
|
523
|
+
else outputError(message);
|
|
524
|
+
return 1;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const warnings = [];
|
|
528
|
+
if (request.secretFile) {
|
|
529
|
+
let stat;
|
|
530
|
+
try {
|
|
531
|
+
stat = fs.statSync(request.secretFile);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
return fail(`could not read the client secret file ${request.secretFile}${error.code ? ` (${error.code})` : ''}`, 'secret_missing');
|
|
534
|
+
}
|
|
535
|
+
if (!stat.isFile()) return fail(`${request.secretFile} is not a file`, 'secret_missing');
|
|
536
|
+
if (process.platform !== 'win32' && (stat.mode & 0o077)) {
|
|
537
|
+
warnings.push(`other users on this machine can read ${request.secretFile}. run: chmod 600 ${request.secretFile}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const pending = {
|
|
542
|
+
auth_type: agentClient.AUTH_TYPE,
|
|
543
|
+
client_id: request.clientId,
|
|
544
|
+
client_secret_file: request.secretFile,
|
|
545
|
+
client_secret_env: request.secretFile ? null : 'ATRIS_CLIENT_SECRET',
|
|
546
|
+
scope: request.scope,
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
let secret;
|
|
550
|
+
let entry;
|
|
551
|
+
try {
|
|
552
|
+
secret = agentClient.readClientSecret(pending, env);
|
|
553
|
+
entry = await agentClient.requestClientToken({ clientId: request.clientId, secret, scope: request.scope }, deps);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
return fail(agentClient.scrub(error && error.message, [secret]), error && error.code);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let secretFile = request.secretFile;
|
|
559
|
+
let copiedSecretTo = null;
|
|
560
|
+
if (!secretFile) {
|
|
561
|
+
// The secret came from ATRIS_CLIENT_SECRET. Keep a private copy so later
|
|
562
|
+
// commands can renew without the variable.
|
|
563
|
+
secretFile = agentClient.managedSecretPath(request.clientId);
|
|
564
|
+
agentClient.writeFileAtomic(secretFile, `${secret}\n`);
|
|
565
|
+
copiedSecretTo = secretFile;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const keptProfile = keepHumanLoginAsProfile(request.clientId);
|
|
569
|
+
saveAgentClientCredentials({ client_id: request.clientId, client_secret_file: secretFile, scope: request.scope });
|
|
570
|
+
agentClient.writeTokenCache(request.clientId, entry);
|
|
571
|
+
|
|
572
|
+
const scopes = agentClient.scopeList(entry.scope);
|
|
573
|
+
if (envValue(env, 'ATRIS_TOKEN')) {
|
|
574
|
+
warnings.push('ATRIS_TOKEN is set in this shell and takes priority. unset it to use the agent client.');
|
|
575
|
+
}
|
|
576
|
+
if (json) {
|
|
577
|
+
output(JSON.stringify({
|
|
578
|
+
ok: true,
|
|
579
|
+
logged_in: true,
|
|
580
|
+
auth_type: agentClient.AUTH_TYPE,
|
|
581
|
+
client_id: request.clientId,
|
|
582
|
+
scopes,
|
|
583
|
+
token_expires_at: entry.expires_at,
|
|
584
|
+
...(copiedSecretTo ? { secret_copied_to: copiedSecretTo } : {}),
|
|
585
|
+
warnings,
|
|
586
|
+
}, null, 2));
|
|
587
|
+
return 0;
|
|
588
|
+
}
|
|
589
|
+
output(`signed in as agent client ${request.clientId}`);
|
|
590
|
+
output(`scopes: ${scopes.length ? scopes.join(', ') : 'all granted to this client'}`);
|
|
591
|
+
output('the token renews on its own before it runs out.');
|
|
592
|
+
if (copiedSecretTo) output(`copied the secret from ATRIS_CLIENT_SECRET to ${copiedSecretTo} (only you can read it).`);
|
|
593
|
+
if (keptProfile) output(`the previous login is saved. switch back: atris switch ${keptProfile} --global`);
|
|
594
|
+
warnings.forEach((line) => outputError(line));
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
433
598
|
async function loginAtris(options = {}) {
|
|
434
599
|
// Support: atris login --token <token> --force
|
|
435
600
|
const args = process.argv.slice(3);
|
|
@@ -440,6 +605,16 @@ async function loginAtris(options = {}) {
|
|
|
440
605
|
const nonInteractive = isNonInteractive(args);
|
|
441
606
|
|
|
442
607
|
try {
|
|
608
|
+
const clientLogin = agentClientLoginRequest(args, process.env);
|
|
609
|
+
if (clientLogin) {
|
|
610
|
+
if (clientLogin.error) {
|
|
611
|
+
if (asJson) console.log(JSON.stringify({ ok: false, logged_in: false, error: clientLogin.error }, null, 2));
|
|
612
|
+
else console.error(clientLogin.error);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
process.exit(await loginAgentClient(clientLogin, { json: asJson }));
|
|
616
|
+
}
|
|
617
|
+
|
|
443
618
|
const existing = loadCredentials();
|
|
444
619
|
|
|
445
620
|
if (wantsAgentToken(args)) {
|
|
@@ -487,12 +662,13 @@ async function loginAtris(options = {}) {
|
|
|
487
662
|
email,
|
|
488
663
|
user_id: userId,
|
|
489
664
|
provider,
|
|
665
|
+
...(existing.client_id ? { auth_type: agentClient.AUTH_TYPE, client_id: existing.client_id } : {}),
|
|
490
666
|
credentials_saved: existing.saved_at || null,
|
|
491
667
|
next: 'atris whoami --json',
|
|
492
668
|
}, null, 2));
|
|
493
669
|
process.exit(0);
|
|
494
670
|
}
|
|
495
|
-
console.log(`Currently signed in as: ${email || userId || 'unknown'}`);
|
|
671
|
+
console.log(`Currently signed in as: ${email || userId || (existing.client_id ? `agent client ${existing.client_id}` : 'unknown')}`);
|
|
496
672
|
if (provider) console.log(`Provider: ${provider}`);
|
|
497
673
|
console.log('Next: atris whoami');
|
|
498
674
|
process.exit(0);
|
|
@@ -608,11 +784,17 @@ function logoutAtris() {
|
|
|
608
784
|
process.exit(0);
|
|
609
785
|
}
|
|
610
786
|
|
|
787
|
+
if (credentials.from_env) {
|
|
788
|
+
console.log(`This agent client (${credentials.client_id}) comes from ATRIS_CLIENT_ID in your environment.`);
|
|
789
|
+
console.log('Unset ATRIS_CLIENT_ID to sign it out.');
|
|
790
|
+
process.exit(0);
|
|
791
|
+
}
|
|
792
|
+
|
|
611
793
|
const profiles = listProfiles();
|
|
612
794
|
const currentName = profileNameFromEmail(credentials?.email);
|
|
613
795
|
|
|
614
796
|
deleteCredentials();
|
|
615
|
-
console.log(`✓ Signed out from ${credentials.email || 'current account'}`);
|
|
797
|
+
console.log(`✓ Signed out from ${credentials.email || (credentials.client_id ? `agent client ${credentials.client_id}` : 'current account')}`);
|
|
616
798
|
|
|
617
799
|
// Remind about other profiles
|
|
618
800
|
const remaining = profiles.filter(p => p !== currentName);
|
|
@@ -658,14 +840,14 @@ async function switchAccount() {
|
|
|
658
840
|
}
|
|
659
841
|
|
|
660
842
|
const current = loadCredentials();
|
|
661
|
-
const currentName =
|
|
843
|
+
const currentName = activeProfileName(current);
|
|
662
844
|
|
|
663
845
|
if (!targetName) {
|
|
664
846
|
// Interactive: show list and let user pick
|
|
665
847
|
console.log('Switch account:\n');
|
|
666
848
|
profiles.forEach((name, i) => {
|
|
667
849
|
const profile = loadProfile(name);
|
|
668
|
-
const email = profile
|
|
850
|
+
const email = profile ? profileLabel(profile, 'unknown') : 'unknown';
|
|
669
851
|
const marker = name === currentName ? ' ← active' : '';
|
|
670
852
|
console.log(` ${i + 1}. ${name}, ${email}${marker}`);
|
|
671
853
|
});
|
|
@@ -709,6 +891,32 @@ async function switchAccount() {
|
|
|
709
891
|
return activateProfile(match, currentName, { global: globalFlag });
|
|
710
892
|
}
|
|
711
893
|
|
|
894
|
+
// A profile can be switched to if it has a login token or is a saved agent client.
|
|
895
|
+
function usableProfile(profile) {
|
|
896
|
+
return Boolean(profile && (profile.token || agentClient.isAgentClientCredential(profile)));
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function profileLabel(profile, name) {
|
|
900
|
+
if (profile?.email) return profile.email;
|
|
901
|
+
if (agentClient.isAgentClientCredential(profile)) return `agent client ${profile.client_id}`;
|
|
902
|
+
return name;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function activeProfileName(credentials) {
|
|
906
|
+
if (agentClient.isAgentClientCredential(credentials)) return agentClient.profileNameForClient(credentials.client_id);
|
|
907
|
+
return profileNameFromEmail(credentials?.email);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Overwrite credentials.json with a profile. An agent client login there is
|
|
911
|
+
// saved as a profile first, so its token cache and secret file stay reachable.
|
|
912
|
+
function writeGlobalCredentials(profile) {
|
|
913
|
+
const kept = preserveAgentClientLogin();
|
|
914
|
+
const credentialsPath = getCredentialsPath();
|
|
915
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
|
|
916
|
+
try { fs.chmodSync(credentialsPath, 0o600); } catch {}
|
|
917
|
+
return kept;
|
|
918
|
+
}
|
|
919
|
+
|
|
712
920
|
function activateProfile(name, currentName, { global = false } = {}) {
|
|
713
921
|
if (name === currentName) {
|
|
714
922
|
console.log(`Already on "${name}".`);
|
|
@@ -716,21 +924,20 @@ function activateProfile(name, currentName, { global = false } = {}) {
|
|
|
716
924
|
}
|
|
717
925
|
|
|
718
926
|
const profile = loadProfile(name);
|
|
719
|
-
if (!profile
|
|
927
|
+
if (!usableProfile(profile)) {
|
|
720
928
|
console.error(`Profile "${name}" is corrupted. Run "atris login" to fix.`);
|
|
721
929
|
process.exit(1);
|
|
722
930
|
}
|
|
723
931
|
|
|
724
932
|
if (global || !getTerminalSessionId()) {
|
|
725
933
|
// Global switch, write to credentials.json (affects all terminals)
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
console.log(`Switched to ${profile.email || name} (global, all terminals)`);
|
|
934
|
+
const kept = writeGlobalCredentials(profile);
|
|
935
|
+
console.log(`Switched to ${profileLabel(profile, name)} (global, all terminals)`);
|
|
936
|
+
if (kept && kept !== name) console.log(`The agent client login is saved. Switch back: atris switch ${kept} --global`);
|
|
730
937
|
} else {
|
|
731
938
|
// Per-terminal switch, write session file (only this terminal)
|
|
732
939
|
setSessionProfile(name);
|
|
733
|
-
console.log(`Switched to ${profile
|
|
940
|
+
console.log(`Switched to ${profileLabel(profile, name)}`);
|
|
734
941
|
}
|
|
735
942
|
}
|
|
736
943
|
|
|
@@ -958,10 +1165,8 @@ function activateGlobal() {
|
|
|
958
1165
|
const name = process.argv[3];
|
|
959
1166
|
if (!name) { process.exit(1); }
|
|
960
1167
|
const profile = loadProfile(name);
|
|
961
|
-
if (!profile
|
|
962
|
-
|
|
963
|
-
fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
|
|
964
|
-
try { fs.chmodSync(credentialsPath, 0o600); } catch {}
|
|
1168
|
+
if (!usableProfile(profile)) { process.exit(1); }
|
|
1169
|
+
writeGlobalCredentials(profile);
|
|
965
1170
|
process.exit(0);
|
|
966
1171
|
}
|
|
967
1172
|
|
|
@@ -974,7 +1179,7 @@ function switchSession() {
|
|
|
974
1179
|
if (!name) { process.exit(1); }
|
|
975
1180
|
|
|
976
1181
|
const profile = loadProfile(name);
|
|
977
|
-
if (!profile
|
|
1182
|
+
if (!usableProfile(profile)) { process.exit(1); }
|
|
978
1183
|
|
|
979
1184
|
// Accept explicit session ID from the shell wrapper (more reliable than
|
|
980
1185
|
// detecting it from inside a child process where TTY env vars may be missing).
|
|
@@ -996,9 +1201,7 @@ function switchSession() {
|
|
|
996
1201
|
setSessionProfile(name);
|
|
997
1202
|
} else {
|
|
998
1203
|
// No terminal ID, global fallback
|
|
999
|
-
|
|
1000
|
-
fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
|
|
1001
|
-
try { fs.chmodSync(credentialsPath, 0o600); } catch {}
|
|
1204
|
+
writeGlobalCredentials(profile);
|
|
1002
1205
|
}
|
|
1003
1206
|
}
|
|
1004
1207
|
process.exit(0);
|
|
@@ -1087,4 +1290,5 @@ module.exports = {
|
|
|
1087
1290
|
persistMintedAgentToken,
|
|
1088
1291
|
ensureBilledCommandAuth,
|
|
1089
1292
|
wantsAgentToken,
|
|
1293
|
+
agentClientLoginRequest,
|
|
1090
1294
|
};
|
package/commands/autoland.js
CHANGED
|
@@ -555,6 +555,21 @@ function showStatus(root, args) {
|
|
|
555
555
|
console.log('');
|
|
556
556
|
console.log(` waiting on a human right now: ${waiting.length}, oldest ${waiting[0].hours}h`);
|
|
557
557
|
}
|
|
558
|
+
// What the next tick's idle sweep will do, read-only (no check runs here).
|
|
559
|
+
if (enabled && policy.janitor !== false && policy.idle_sweep !== false) {
|
|
560
|
+
try {
|
|
561
|
+
const idle = require('../lib/idle-review-sweep').sweepIdleReviews(root, { tasks, policy, dryRun: true });
|
|
562
|
+
const days = Number(policy.idle_close_days) > 0 ? Number(policy.idle_close_days) : 7;
|
|
563
|
+
if (idle.closed.length) {
|
|
564
|
+
console.log(` closing on the next tick: ${idle.closed.length} idle over ${days} days that can never land on their own, each with a plain reason.`);
|
|
565
|
+
}
|
|
566
|
+
if (idle.human_waiting.length) {
|
|
567
|
+
console.log(` staying with you: ${idle.human_waiting.length} in money, deploys, security, or customer work; idle time never closes these.`);
|
|
568
|
+
}
|
|
569
|
+
} catch {
|
|
570
|
+
// Preview only.
|
|
571
|
+
}
|
|
572
|
+
}
|
|
558
573
|
console.log('');
|
|
559
574
|
if (!enabled) console.log(" flip it on: atris autoland on (one decision; everything after is receipts)");
|
|
560
575
|
console.log('');
|
|
@@ -798,9 +813,11 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
798
813
|
.filter((task) => String(task.review?.approval_status || task.metadata?.approval_status || 'pending') === 'pending')
|
|
799
814
|
.length;
|
|
800
815
|
const accept = runOwnCli(root, cliArgs, acceptSweepTimeoutMs(pendingReviewRows));
|
|
816
|
+
let acceptResults = [];
|
|
801
817
|
try {
|
|
802
818
|
const parsed = JSON.parse(accept.stdout);
|
|
803
819
|
const results = Array.isArray(parsed.results) ? parsed.results : [];
|
|
820
|
+
acceptResults = results;
|
|
804
821
|
// A refused sweep (ok:false, a guard tripped, policy race) carries no
|
|
805
822
|
// summary fields. Name the reason instead of leaving nulls that read as
|
|
806
823
|
// "no work": a blind heartbeat must say WHY it is blind.
|
|
@@ -963,6 +980,51 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
963
980
|
recordLandingSweepState(state, null, { error: receipt.reap_error });
|
|
964
981
|
}
|
|
965
982
|
|
|
983
|
+
// 3c2. idle work resolves itself, every tick. Finished work that sat still
|
|
984
|
+
// past the grace period (policy.idle_close_days, default 7) and can never
|
|
985
|
+
// land on its own is closed with a plain reason; done and long-idle rows
|
|
986
|
+
// leave the list. This used to run only when someone opened a session, so
|
|
987
|
+
// nights and trips piled up work nobody would ever land. Protected lanes
|
|
988
|
+
// (money, deploys, security, customer, outward) are never closed by idle
|
|
989
|
+
// time. Off switch: policy.idle_sweep === false or policy.janitor === false.
|
|
990
|
+
if (policy.janitor !== false && policy.idle_sweep !== false) {
|
|
991
|
+
try {
|
|
992
|
+
const liveReasons = new Map(acceptResults
|
|
993
|
+
.filter((r) => r && r.ref && r.reason && r.action !== 'accepted')
|
|
994
|
+
.map((r) => [r.ref, r.reason]));
|
|
995
|
+
const kept = require('../lib/task-list-keeper').keepWorkspaceTaskList(root, { actor: 'autoland', includeDone: false });
|
|
996
|
+
const idle = require('../lib/idle-review-sweep').sweepIdleReviews(root, {
|
|
997
|
+
tasks: readProjection(root),
|
|
998
|
+
policy,
|
|
999
|
+
liveReasons,
|
|
1000
|
+
});
|
|
1001
|
+
const putAway = (kept.put_away || []).length + (kept.reaped || []).length;
|
|
1002
|
+
if (putAway + idle.closed.length > 0) require('./task').refreshKeptTaskList(root);
|
|
1003
|
+
receipt.idle_sweep = {
|
|
1004
|
+
closed: idle.closed.map((c) => ({ ref: c.ref, reason: c.reason })),
|
|
1005
|
+
put_away: putAway,
|
|
1006
|
+
human_waiting: idle.human_waiting.length,
|
|
1007
|
+
};
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
receipt.idle_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
const { appendOvernightNote, tickSummaryLine } = require('../lib/cleanup-summary');
|
|
1014
|
+
const line = tickSummaryLine({
|
|
1015
|
+
landed: (receipt.landed || []).length,
|
|
1016
|
+
closed: receipt.idle_sweep ? receipt.idle_sweep.closed.length : 0,
|
|
1017
|
+
putAway: receipt.idle_sweep ? receipt.idle_sweep.put_away : 0,
|
|
1018
|
+
needYou: receipt.idle_sweep ? receipt.idle_sweep.human_waiting : protectedWaiting.length,
|
|
1019
|
+
});
|
|
1020
|
+
if (line) {
|
|
1021
|
+
appendOvernightNote(root, line);
|
|
1022
|
+
receipt.summary_line = line;
|
|
1023
|
+
}
|
|
1024
|
+
} catch {
|
|
1025
|
+
// The journal line is a courtesy; it never fails the tick.
|
|
1026
|
+
}
|
|
1027
|
+
|
|
966
1028
|
// 3d. wish sweep: queued wishes are allowed to wake a mission without a
|
|
967
1029
|
// foreground `atris wish` session, but they must never endanger landing.
|
|
968
1030
|
try {
|
package/commands/brain.js
CHANGED
|
@@ -1140,6 +1140,10 @@ function taskEpisodeScorecard(root, episode, workspace, ts = new Date().toISOStr
|
|
|
1140
1140
|
task_title: episode.state && episode.state.title || null,
|
|
1141
1141
|
task_tag: episode.state && episode.state.tag || null,
|
|
1142
1142
|
actor: episode.action && episode.action.actor || null,
|
|
1143
|
+
// Who built the work. `actor` is the reviewer; trust is earned by the builder.
|
|
1144
|
+
builder: episode.state && episode.state.metadata && episode.state.metadata.built_by
|
|
1145
|
+
|| episode.state && episode.state.claimed_by
|
|
1146
|
+
|| null,
|
|
1143
1147
|
reward: Number.isFinite(reward) ? reward : 0,
|
|
1144
1148
|
reward_source: episode.reward && episode.reward.source || 'task_review',
|
|
1145
1149
|
lesson: episode.lesson || '',
|
package/commands/engine.js
CHANGED
|
@@ -57,6 +57,8 @@ const {
|
|
|
57
57
|
rosterJobNameError,
|
|
58
58
|
rosterJobRole,
|
|
59
59
|
readRosterState,
|
|
60
|
+
retiredModelsNamed,
|
|
61
|
+
RETIRED_CLAUDE_MODELS,
|
|
60
62
|
customRosterJobKeys,
|
|
61
63
|
setRosterPick,
|
|
62
64
|
confirmRoster,
|
|
@@ -1161,6 +1163,7 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1161
1163
|
jobs,
|
|
1162
1164
|
expiring,
|
|
1163
1165
|
renew_command: expiring.length ? RENEW_COMMAND : null,
|
|
1166
|
+
upgraded: upgradedModels(state),
|
|
1164
1167
|
team: team.rows,
|
|
1165
1168
|
warnings: [...state.session.warnings, ...state.project.warnings, ...state.machine.warnings, ...team.warnings],
|
|
1166
1169
|
files: {
|
|
@@ -1172,6 +1175,25 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1172
1175
|
};
|
|
1173
1176
|
}
|
|
1174
1177
|
|
|
1178
|
+
// Retired models a roster file still names. Every read runs them as their
|
|
1179
|
+
// replacement, so this is only a heads-up; nobody has to edit the file.
|
|
1180
|
+
function upgradedModels(state) {
|
|
1181
|
+
const found = [];
|
|
1182
|
+
for (const layer of [state.session, state.project, state.machine]) {
|
|
1183
|
+
if (!layer || layer.format === 'none' || !layer.path) continue;
|
|
1184
|
+
let text = '';
|
|
1185
|
+
try { text = fs.readFileSync(layer.path, 'utf8'); } catch { continue; }
|
|
1186
|
+
for (const from of retiredModelsNamed(text)) {
|
|
1187
|
+
if (!found.some((item) => item.from === from)) found.push({ from, to: RETIRED_CLAUDE_MODELS[from] });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return found;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function renderUpgradedLine(upgraded) {
|
|
1194
|
+
return (upgraded || []).map((item) => `note: ${modelLabel(item.from)} is retired, so your roster runs ${modelLabel(item.to)} in its place. no edit needed.`).join('\n');
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1175
1197
|
// Every worker, in any job, whose end date is today or within the next
|
|
1176
1198
|
// seven days. Workers already past their date show on their own lines. A
|
|
1177
1199
|
// job set for this shell only is skipped: roster confirm does not renew it.
|
|
@@ -1367,7 +1389,7 @@ function renderRosterWarnings(warnings) {
|
|
|
1367
1389
|
|
|
1368
1390
|
function renderRosterReport(report, { width } = {}) {
|
|
1369
1391
|
const stale = (report.stale || []).map((item) => item.text);
|
|
1370
|
-
const jobs = [renderJobRoster(report.jobs, { width }), renderExpiringLine(report.expiring), ...stale].filter(Boolean).join('\n');
|
|
1392
|
+
const jobs = [renderJobRoster(report.jobs, { width }), renderExpiringLine(report.expiring), ...stale, renderUpgradedLine(report.upgraded)].filter(Boolean).join('\n');
|
|
1371
1393
|
return [jobs, renderTeamRoster(report.team, { width }), renderRosterWarnings(report.warnings)]
|
|
1372
1394
|
.filter(Boolean)
|
|
1373
1395
|
.join('\n\n');
|