atris 3.64.0 → 3.65.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 +4 -0
- package/bin/atris.js +17 -0
- package/commands/agent-spawn.js +1 -0
- package/commands/engine.js +181 -63
- package/commands/hire.js +189 -0
- package/commands/mail.js +163 -0
- package/commands/mission.js +3 -2
- package/commands/stream.js +22 -1
- package/commands/team.js +355 -67
- package/lib/auto-accept-certified.js +83 -9
- package/lib/business-target.js +131 -0
- package/lib/engine-processes.js +430 -0
- package/lib/engine-registry.js +16 -3
- package/lib/known-commands.js +1 -1
- package/lib/member-identity.js +56 -0
- package/lib/roster-models.js +11 -0
- package/lib/roster-stale.js +241 -0
- package/lib/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -0
- package/package.json +1 -1
|
@@ -457,8 +457,61 @@ function parseVerifyCommand(verify) {
|
|
|
457
457
|
};
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// Members record checks as one command or several joined with `&&`
|
|
461
|
+
// ("npm run test:a && node scripts/test-b.mjs"). Each step must pass the
|
|
462
|
+
// same allowlist on its own; `&&` only runs them in order and stops at the
|
|
463
|
+
// first failure, still with shell:false, so a chain can do nothing its steps
|
|
464
|
+
// could not do one at a time. One leading `cd` applies to every step.
|
|
465
|
+
const MAX_VERIFY_CHAIN_STEPS = 8;
|
|
466
|
+
|
|
467
|
+
function splitTopLevelAnd(command) {
|
|
468
|
+
const parts = [];
|
|
469
|
+
let current = '';
|
|
470
|
+
let quote = null;
|
|
471
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
472
|
+
const char = command[i];
|
|
473
|
+
if (quote) {
|
|
474
|
+
if (char === quote) quote = null;
|
|
475
|
+
current += char;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (char === "'" || char === '"') {
|
|
479
|
+
quote = char;
|
|
480
|
+
current += char;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (char === '&' && command[i + 1] === '&') {
|
|
484
|
+
parts.push(current.trim());
|
|
485
|
+
current = '';
|
|
486
|
+
i += 1;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
current += char;
|
|
490
|
+
}
|
|
491
|
+
parts.push(current.trim());
|
|
492
|
+
return parts;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function parseVerifyChain(verify) {
|
|
496
|
+
const cmd = String(verify || '').trim();
|
|
497
|
+
if (!cmd) return { ok: false, reason: 'no_verify_command' };
|
|
498
|
+
const prefixed = parseCdPrefix(cmd);
|
|
499
|
+
if (!prefixed.ok) return prefixed;
|
|
500
|
+
const parts = splitTopLevelAnd(prefixed.command);
|
|
501
|
+
if (parts.length > MAX_VERIFY_CHAIN_STEPS || parts.some((part) => !part || /^cd(\s|$)/.test(part))) {
|
|
502
|
+
return { ok: false, reason: 'verify_command_not_allowed' };
|
|
503
|
+
}
|
|
504
|
+
const steps = [];
|
|
505
|
+
for (const part of parts) {
|
|
506
|
+
const parsed = parseVerifyCommand(part);
|
|
507
|
+
if (!parsed.ok) return parsed;
|
|
508
|
+
steps.push({ ...parsed, ...(prefixed.cwd ? { cwd: prefixed.cwd } : {}) });
|
|
509
|
+
}
|
|
510
|
+
return { ok: true, steps, ...(prefixed.cwd ? { cwd: prefixed.cwd } : {}) };
|
|
511
|
+
}
|
|
512
|
+
|
|
460
513
|
function isAutoCertifyVerifyCommandAllowed(verify) {
|
|
461
|
-
return
|
|
514
|
+
return parseVerifyChain(verify).ok;
|
|
462
515
|
}
|
|
463
516
|
|
|
464
517
|
// Re-entrancy guard: every verify child carries ATRIS_VERIFY_IN_PROGRESS=1
|
|
@@ -468,12 +521,20 @@ function isAutoCertifyVerifyCommandAllowed(verify) {
|
|
|
468
521
|
// `atris task status` and the read path re-ran the verify. Refuse loudly.
|
|
469
522
|
const VERIFY_IN_PROGRESS_ENV = 'ATRIS_VERIFY_IN_PROGRESS';
|
|
470
523
|
|
|
471
|
-
// Spawn cap: a
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
|
|
524
|
+
// Spawn cap: bounds a runaway loop inside one process. The recursion that
|
|
525
|
+
// fork-bombed the fleet is stopped by VERIFY_IN_PROGRESS_ENV above; this cap
|
|
526
|
+
// is the backstop. It was 25, which starved the review queue: every hourly
|
|
527
|
+
// tick re-checked the same first 25 rows and the other 141 waiting rows were
|
|
528
|
+
// refused as verify_spawn_cap forever (measured 2026-10-01). Override with
|
|
529
|
+
// ATRIS_VERIFY_SPAWN_CAP.
|
|
530
|
+
const DEFAULT_VERIFY_SPAWN_CAP = 150;
|
|
475
531
|
let verifySpawnCount = 0;
|
|
476
532
|
|
|
533
|
+
function verifySpawnCap() {
|
|
534
|
+
const raw = Number(process.env.ATRIS_VERIFY_SPAWN_CAP);
|
|
535
|
+
return Number.isInteger(raw) && raw > 0 ? Math.min(raw, 1000) : DEFAULT_VERIFY_SPAWN_CAP;
|
|
536
|
+
}
|
|
537
|
+
|
|
477
538
|
function verifyRefusal(unrunnableCause, extra) {
|
|
478
539
|
return {
|
|
479
540
|
ok: false,
|
|
@@ -490,14 +551,26 @@ function runVerifyCommand(verify, workspaceRoot) {
|
|
|
490
551
|
detail: `${VERIFY_IN_PROGRESS_ENV} is set: a verify is already running up the call chain; refusing to nest another one`,
|
|
491
552
|
});
|
|
492
553
|
}
|
|
493
|
-
|
|
554
|
+
const chain = parseVerifyChain(verify);
|
|
555
|
+
if (!chain.ok) return chain;
|
|
556
|
+
let result = null;
|
|
557
|
+
for (const step of chain.steps) {
|
|
558
|
+
result = runVerifyStep(step, workspaceRoot);
|
|
559
|
+
if (!result.ok) {
|
|
560
|
+
return chain.steps.length > 1 ? { ...result, failed_step: step.argv.join(' ') } : result;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function runVerifyStep(parsed, workspaceRoot) {
|
|
567
|
+
const cap = verifySpawnCap();
|
|
568
|
+
if (verifySpawnCount >= cap) {
|
|
494
569
|
return verifyRefusal('verify_spawn_cap', {
|
|
495
|
-
detail: `this process already ran ${verifySpawnCount} verifies (cap ${
|
|
570
|
+
detail: `this process already ran ${verifySpawnCount} verifies (cap ${cap}); refusing more`,
|
|
496
571
|
});
|
|
497
572
|
}
|
|
498
573
|
verifySpawnCount += 1;
|
|
499
|
-
const parsed = parseVerifyCommand(verify);
|
|
500
|
-
if (!parsed.ok) return parsed;
|
|
501
574
|
const cwdCheck = validateCommandCwd(parsed, workspaceRoot);
|
|
502
575
|
if (!cwdCheck.ok) return cwdCheck;
|
|
503
576
|
const gitPathCheck = validateGitWorktreePath(parsed.argv, workspaceRoot, cwdCheck.cwd);
|
|
@@ -1250,6 +1323,7 @@ module.exports = {
|
|
|
1250
1323
|
sniffedProtectedLane,
|
|
1251
1324
|
evaluateAutoAccept,
|
|
1252
1325
|
isAutoCertifyVerifyCommandAllowed,
|
|
1326
|
+
parseVerifyChain,
|
|
1253
1327
|
isAgentCertified,
|
|
1254
1328
|
parseVerifyCommand,
|
|
1255
1329
|
repoHygieneGate,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared plumbing for business-scoped HTTP commands (`atris hire`, `atris hires`,
|
|
4
|
+
// `atris mail`): parse flags, find which business to act on, get a login token.
|
|
5
|
+
// These commands are HTTP clients of /api/business/{id}/...; no backend code here.
|
|
6
|
+
|
|
7
|
+
const { apiRequestJson } = require('../utils/api');
|
|
8
|
+
const { ensureValidCredentials } = require('../utils/auth');
|
|
9
|
+
const { findBusiness } = require('../commands/feed');
|
|
10
|
+
const { oneLine } = require('./developer-api');
|
|
11
|
+
|
|
12
|
+
const NO_BUSINESS = 'no business here. run this inside a business folder, or add --business <slug>';
|
|
13
|
+
|
|
14
|
+
// --help, --json and --business are allowed on every command.
|
|
15
|
+
const COMMON_SWITCHES = ['help', 'json'];
|
|
16
|
+
const COMMON_VALUES = ['business'];
|
|
17
|
+
|
|
18
|
+
// --key value, --key=value, and yes/no switches that never take a value.
|
|
19
|
+
// `spec` is { switches: [...], values: [...] }: the only flags the command
|
|
20
|
+
// takes, beyond the common ones. Anything else comes back as `error`, and
|
|
21
|
+
// the command must stop before making any request:
|
|
22
|
+
// an unknown flag (--dry-run on a command without it),
|
|
23
|
+
// a value on a switch (--yes=false is refused, never read as yes),
|
|
24
|
+
// a value flag with nothing after it (--business ""),
|
|
25
|
+
// the same value flag twice.
|
|
26
|
+
// A plain array is the old lenient form (switch names only), kept for callers
|
|
27
|
+
// that want it; it never reports errors.
|
|
28
|
+
function parseArgs(argv = [], spec = []) {
|
|
29
|
+
const strict = !Array.isArray(spec);
|
|
30
|
+
const switchNames = strict ? (spec.switches || []) : spec;
|
|
31
|
+
const valueNames = strict ? (spec.values || []) : [];
|
|
32
|
+
const switches = new Set([...switchNames, ...COMMON_SWITCHES, 'yes']);
|
|
33
|
+
const allowed = new Set([...switchNames, ...valueNames, ...COMMON_SWITCHES, ...COMMON_VALUES]);
|
|
34
|
+
const flags = {};
|
|
35
|
+
const pos = [];
|
|
36
|
+
let error = '';
|
|
37
|
+
const fail = (message) => { if (!error) error = message; };
|
|
38
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
39
|
+
const arg = String(argv[i]);
|
|
40
|
+
if (arg === '-h') { flags.help = true; continue; }
|
|
41
|
+
if (arg === '-y') {
|
|
42
|
+
if (strict && !allowed.has('yes')) fail('unknown option -y');
|
|
43
|
+
else flags.yes = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg.startsWith('--')) {
|
|
47
|
+
const eq = arg.indexOf('=');
|
|
48
|
+
const raw = eq > 0 ? arg.slice(0, eq) : arg;
|
|
49
|
+
const key = raw.slice(2).replace(/-/g, '_');
|
|
50
|
+
if (strict && !allowed.has(key)) { fail(`unknown option ${raw}`); continue; }
|
|
51
|
+
if (switches.has(key)) {
|
|
52
|
+
if (eq < 0) flags[key] = true;
|
|
53
|
+
else if (strict) fail(`${raw} is a switch and takes no value: use ${raw} on its own, or leave it off`);
|
|
54
|
+
else flags[key] = arg.slice(eq + 1);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
let value = true;
|
|
58
|
+
if (eq > 0) value = arg.slice(eq + 1);
|
|
59
|
+
else if (argv[i + 1] != null && !String(argv[i + 1]).startsWith('--')) { value = String(argv[i + 1]); i += 1; }
|
|
60
|
+
if (strict && (typeof value !== 'string' || !value.trim())) { fail(`${raw} needs a value`); continue; }
|
|
61
|
+
if (strict && Object.prototype.hasOwnProperty.call(flags, key)) { fail(`${raw} was given twice`); continue; }
|
|
62
|
+
flags[key] = value;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (strict && arg.length > 1 && arg.startsWith('-')) { fail(`unknown option ${arg}`); continue; }
|
|
66
|
+
pos.push(arg);
|
|
67
|
+
}
|
|
68
|
+
return strict ? { flags, pos, error } : { flags, pos };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A --business that was given must name a business. An empty or missing
|
|
72
|
+
// value is an error and never falls back to the current folder's business.
|
|
73
|
+
// Without --business: the nearest .atris/business.json above the current
|
|
74
|
+
// folder. Returns { id, name }, { error }, or null when none is found.
|
|
75
|
+
function resolveBusiness(flags = {}, deps = {}) {
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(flags, 'business')) {
|
|
77
|
+
const given = typeof flags.business === 'string' ? flags.business.trim() : '';
|
|
78
|
+
if (!given) return { error: '--business needs a business slug or id' };
|
|
79
|
+
return { id: given, name: given };
|
|
80
|
+
}
|
|
81
|
+
const found = (deps.findBusiness || findBusiness)(deps.cwd || process.cwd());
|
|
82
|
+
return found && found.businessId ? { id: found.businessId, name: found.name || 'this business' } : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The business to act on, or says why not (through `err`) and returns null.
|
|
86
|
+
function businessOrSay(flags, deps, err) {
|
|
87
|
+
const business = resolveBusiness(flags, deps);
|
|
88
|
+
if (!business) { err(NO_BUSINESS); return null; }
|
|
89
|
+
if (business.error) { err(business.error); return null; }
|
|
90
|
+
return business;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loginToken(deps = {}) {
|
|
94
|
+
if (deps.token) return deps.token;
|
|
95
|
+
const ensure = deps.ensureCredentials || (() => ensureValidCredentials(apiRequestJson));
|
|
96
|
+
const auth = await ensure();
|
|
97
|
+
return auth && !auth.error && auth.credentials && auth.credentials.token ? auth.credentials.token : '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// FastAPI errors arrive as {"detail": "..."}; keep them to one plain line.
|
|
101
|
+
function errorText(result) {
|
|
102
|
+
if (!result) return 'request failed';
|
|
103
|
+
if (Number(result.status) === 404 && !(result.data && result.data.detail)) {
|
|
104
|
+
return 'backend rolling out, try again shortly';
|
|
105
|
+
}
|
|
106
|
+
return oneLine((result.data && (result.data.detail || result.data.error)) || result.error || `request failed (${result.status})`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// "2026-10-02T09:00:00+00:00" -> "in 9 h" / "6 h ago"
|
|
110
|
+
function relativeTime(iso, now = Date.now()) {
|
|
111
|
+
const at = Date.parse(iso || '');
|
|
112
|
+
if (!Number.isFinite(at)) return '';
|
|
113
|
+
const mins = Math.round((at - now) / 60000);
|
|
114
|
+
const abs = Math.abs(mins);
|
|
115
|
+
const span = abs < 1 ? 'now' : abs < 90 ? `${abs} min` : abs < 48 * 60 ? `${Math.round(abs / 60)} h` : `${Math.round(abs / 1440)} days`;
|
|
116
|
+
if (span === 'now') return 'just now';
|
|
117
|
+
return mins >= 0 ? `in ${span}` : `${span} ago`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function dollars(value) {
|
|
121
|
+
if (value == null || !Number.isFinite(Number(value))) return '-';
|
|
122
|
+
const n = Number(value);
|
|
123
|
+
return Number.isInteger(n) ? `$${n}` : `$${n.toFixed(2)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function clip(text, width) {
|
|
127
|
+
const s = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
|
|
128
|
+
return s.length > width ? `${s.slice(0, width - 1)}…` : s;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { NO_BUSINESS, parseArgs, resolveBusiness, businessOrSay, loginToken, errorText, relativeTime, dollars, clip };
|