atris 3.64.0 → 3.66.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/atris/skills/engines/SKILL.md +2 -2
- package/bin/atris.js +45 -2
- package/commands/agent-spawn.js +1 -0
- package/commands/autoland.js +62 -0
- package/commands/brain.js +4 -0
- package/commands/engine.js +203 -63
- package/commands/hire.js +189 -0
- package/commands/link.js +367 -0
- package/commands/loops.js +4 -1
- package/commands/mail.js +163 -0
- package/commands/member.js +8 -1
- package/commands/mission.js +3 -2
- package/commands/orb.js +1 -1
- package/commands/stream.js +22 -1
- package/commands/team.js +355 -67
- package/commands/xp.js +2 -0
- package/commands/youtube.js +1 -1
- package/lib/auto-accept-certified.js +172 -18
- package/lib/business-target.js +131 -0
- package/lib/cleanup-summary.js +129 -0
- package/lib/engine-ask.js +3 -2
- package/lib/engine-processes.js +430 -0
- package/lib/engine-registry.js +219 -18
- 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-identity.js +56 -0
- package/lib/member-result.js +303 -0
- package/lib/roster-models.js +12 -1
- package/lib/roster-stale.js +241 -0
- package/lib/runner-command.js +4 -4
- package/lib/task-list-keeper.js +25 -4
- package/lib/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -0
- 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
|
@@ -6,7 +6,8 @@ const path = require('path');
|
|
|
6
6
|
const { taskProofState, taskProofExecutionState } = require('./task-proof');
|
|
7
7
|
const { extractReceiptEvidence } = require('./receipt-evidence');
|
|
8
8
|
const reviewIntegrity = require('./review-integrity');
|
|
9
|
-
const { computeTrustTier } = require('./trust-tiers');
|
|
9
|
+
const { computeTrustTier, taskTrustSubject } = require('./trust-tiers');
|
|
10
|
+
const { rewriteWorktreeVerify, missingOnMain } = require('./verify-worktree-rewrite');
|
|
10
11
|
const { policyLessonsForFiles, readPolicyLessons } = require('./policy-lessons');
|
|
11
12
|
const { skillEvalGate } = require('./skill-eval-gate');
|
|
12
13
|
|
|
@@ -457,8 +458,61 @@ function parseVerifyCommand(verify) {
|
|
|
457
458
|
};
|
|
458
459
|
}
|
|
459
460
|
|
|
461
|
+
// Members record checks as one command or several joined with `&&`
|
|
462
|
+
// ("npm run test:a && node scripts/test-b.mjs"). Each step must pass the
|
|
463
|
+
// same allowlist on its own; `&&` only runs them in order and stops at the
|
|
464
|
+
// first failure, still with shell:false, so a chain can do nothing its steps
|
|
465
|
+
// could not do one at a time. One leading `cd` applies to every step.
|
|
466
|
+
const MAX_VERIFY_CHAIN_STEPS = 8;
|
|
467
|
+
|
|
468
|
+
function splitTopLevelAnd(command) {
|
|
469
|
+
const parts = [];
|
|
470
|
+
let current = '';
|
|
471
|
+
let quote = null;
|
|
472
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
473
|
+
const char = command[i];
|
|
474
|
+
if (quote) {
|
|
475
|
+
if (char === quote) quote = null;
|
|
476
|
+
current += char;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (char === "'" || char === '"') {
|
|
480
|
+
quote = char;
|
|
481
|
+
current += char;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (char === '&' && command[i + 1] === '&') {
|
|
485
|
+
parts.push(current.trim());
|
|
486
|
+
current = '';
|
|
487
|
+
i += 1;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
current += char;
|
|
491
|
+
}
|
|
492
|
+
parts.push(current.trim());
|
|
493
|
+
return parts;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function parseVerifyChain(verify) {
|
|
497
|
+
const cmd = String(verify || '').trim();
|
|
498
|
+
if (!cmd) return { ok: false, reason: 'no_verify_command' };
|
|
499
|
+
const prefixed = parseCdPrefix(cmd);
|
|
500
|
+
if (!prefixed.ok) return prefixed;
|
|
501
|
+
const parts = splitTopLevelAnd(prefixed.command);
|
|
502
|
+
if (parts.length > MAX_VERIFY_CHAIN_STEPS || parts.some((part) => !part || /^cd(\s|$)/.test(part))) {
|
|
503
|
+
return { ok: false, reason: 'verify_command_not_allowed' };
|
|
504
|
+
}
|
|
505
|
+
const steps = [];
|
|
506
|
+
for (const part of parts) {
|
|
507
|
+
const parsed = parseVerifyCommand(part);
|
|
508
|
+
if (!parsed.ok) return parsed;
|
|
509
|
+
steps.push({ ...parsed, ...(prefixed.cwd ? { cwd: prefixed.cwd } : {}) });
|
|
510
|
+
}
|
|
511
|
+
return { ok: true, steps, ...(prefixed.cwd ? { cwd: prefixed.cwd } : {}) };
|
|
512
|
+
}
|
|
513
|
+
|
|
460
514
|
function isAutoCertifyVerifyCommandAllowed(verify) {
|
|
461
|
-
return
|
|
515
|
+
return parseVerifyChain(verify).ok;
|
|
462
516
|
}
|
|
463
517
|
|
|
464
518
|
// Re-entrancy guard: every verify child carries ATRIS_VERIFY_IN_PROGRESS=1
|
|
@@ -468,12 +522,20 @@ function isAutoCertifyVerifyCommandAllowed(verify) {
|
|
|
468
522
|
// `atris task status` and the read path re-ran the verify. Refuse loudly.
|
|
469
523
|
const VERIFY_IN_PROGRESS_ENV = 'ATRIS_VERIFY_IN_PROGRESS';
|
|
470
524
|
|
|
471
|
-
// Spawn cap: a
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
|
|
525
|
+
// Spawn cap: bounds a runaway loop inside one process. The recursion that
|
|
526
|
+
// fork-bombed the fleet is stopped by VERIFY_IN_PROGRESS_ENV above; this cap
|
|
527
|
+
// is the backstop. It was 25, which starved the review queue: every hourly
|
|
528
|
+
// tick re-checked the same first 25 rows and the other 141 waiting rows were
|
|
529
|
+
// refused as verify_spawn_cap forever (measured 2026-10-01). Override with
|
|
530
|
+
// ATRIS_VERIFY_SPAWN_CAP.
|
|
531
|
+
const DEFAULT_VERIFY_SPAWN_CAP = 150;
|
|
475
532
|
let verifySpawnCount = 0;
|
|
476
533
|
|
|
534
|
+
function verifySpawnCap() {
|
|
535
|
+
const raw = Number(process.env.ATRIS_VERIFY_SPAWN_CAP);
|
|
536
|
+
return Number.isInteger(raw) && raw > 0 ? Math.min(raw, 1000) : DEFAULT_VERIFY_SPAWN_CAP;
|
|
537
|
+
}
|
|
538
|
+
|
|
477
539
|
function verifyRefusal(unrunnableCause, extra) {
|
|
478
540
|
return {
|
|
479
541
|
ok: false,
|
|
@@ -490,14 +552,26 @@ function runVerifyCommand(verify, workspaceRoot) {
|
|
|
490
552
|
detail: `${VERIFY_IN_PROGRESS_ENV} is set: a verify is already running up the call chain; refusing to nest another one`,
|
|
491
553
|
});
|
|
492
554
|
}
|
|
493
|
-
|
|
555
|
+
const chain = parseVerifyChain(verify);
|
|
556
|
+
if (!chain.ok) return chain;
|
|
557
|
+
let result = null;
|
|
558
|
+
for (const step of chain.steps) {
|
|
559
|
+
result = runVerifyStep(step, workspaceRoot);
|
|
560
|
+
if (!result.ok) {
|
|
561
|
+
return chain.steps.length > 1 ? { ...result, failed_step: step.argv.join(' ') } : result;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function runVerifyStep(parsed, workspaceRoot) {
|
|
568
|
+
const cap = verifySpawnCap();
|
|
569
|
+
if (verifySpawnCount >= cap) {
|
|
494
570
|
return verifyRefusal('verify_spawn_cap', {
|
|
495
|
-
detail: `this process already ran ${verifySpawnCount} verifies (cap ${
|
|
571
|
+
detail: `this process already ran ${verifySpawnCount} verifies (cap ${cap}); refusing more`,
|
|
496
572
|
});
|
|
497
573
|
}
|
|
498
574
|
verifySpawnCount += 1;
|
|
499
|
-
const parsed = parseVerifyCommand(verify);
|
|
500
|
-
if (!parsed.ok) return parsed;
|
|
501
575
|
const cwdCheck = validateCommandCwd(parsed, workspaceRoot);
|
|
502
576
|
if (!cwdCheck.ok) return cwdCheck;
|
|
503
577
|
const gitPathCheck = validateGitWorktreePath(parsed.argv, workspaceRoot, cwdCheck.cwd);
|
|
@@ -1018,6 +1092,79 @@ function sniffedProtectedLane(task) {
|
|
|
1018
1092
|
return null;
|
|
1019
1093
|
}
|
|
1020
1094
|
|
|
1095
|
+
// The protected lane a task sits in, or null: a denied tag (whole word,
|
|
1096
|
+
// plural-stripped, the same match the landing gate uses), a lane declared in
|
|
1097
|
+
// its text, or, for untagged rows, lane terms in its title or objective.
|
|
1098
|
+
// Every automatic closer must leave these for a human.
|
|
1099
|
+
function deniedTagOf(task) {
|
|
1100
|
+
const tag = String((task && task.tag) || '').trim().toLowerCase();
|
|
1101
|
+
return [...DENIED_TAGS].find((d) =>
|
|
1102
|
+
tag === d || tag.split(/[^a-z0-9]+/).some((w) => w === d || w.replace(/s$/, '') === d)) || null;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function protectedLaneOf(task) {
|
|
1106
|
+
if (!task || typeof task !== 'object') return null;
|
|
1107
|
+
const metadata = task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
1108
|
+
const view = { ...task, objective: task.objective || metadata.objective || '', metadata };
|
|
1109
|
+
const denied = deniedTagOf(view);
|
|
1110
|
+
if (denied) return denied;
|
|
1111
|
+
if (declaredProtectedLane(view)) return 'declared';
|
|
1112
|
+
if (!String(view.tag || '').trim()) {
|
|
1113
|
+
const sniffed = sniffedProtectedLane(view);
|
|
1114
|
+
if (sniffed) return sniffed.lane;
|
|
1115
|
+
}
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// The check the landing pass actually runs. A check recorded inside an
|
|
1120
|
+
// agent's temporary copy is rewritten to run from the main checkout; when the
|
|
1121
|
+
// file it runs only exists on the agent's branch, that is reported in plain
|
|
1122
|
+
// words instead of a bare "not on the list this machine may run".
|
|
1123
|
+
function resolveTaskVerify(task) {
|
|
1124
|
+
const recorded = String((task.metadata || {}).verify || '').trim();
|
|
1125
|
+
if (!recorded) return { verify: '', recorded: '', rewritten: false, missing: null };
|
|
1126
|
+
const root = task.workspace_root || process.cwd();
|
|
1127
|
+
const rewrite = rewriteWorktreeVerify(recorded, root);
|
|
1128
|
+
const verify = rewrite.command || recorded;
|
|
1129
|
+
let missing = null;
|
|
1130
|
+
if (rewrite.rewritten) {
|
|
1131
|
+
const chain = parseVerifyChain(verify);
|
|
1132
|
+
if (chain.ok) missing = missingOnMain(chain.steps, root);
|
|
1133
|
+
}
|
|
1134
|
+
return { verify, recorded, rewritten: rewrite.rewritten, missing };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Why the landing pass could never rerun this task's check from main, or
|
|
1138
|
+
// null when it can. Independent of the gate's refusal order, so a row held
|
|
1139
|
+
// for another reason still reports a check that can never run.
|
|
1140
|
+
function verifyRunnability(task) {
|
|
1141
|
+
const resolved = resolveTaskVerify(task);
|
|
1142
|
+
if (!resolved.verify) return 'no_verify_command';
|
|
1143
|
+
if (!isAutoCertifyVerifyCommandAllowed(resolved.verify)) return 'verify_command_not_allowed';
|
|
1144
|
+
if (resolved.missing) return 'verify_only_on_agent_branch';
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function agentBranchOnlyResult(ref, resolved) {
|
|
1149
|
+
return {
|
|
1150
|
+
eligible: false,
|
|
1151
|
+
ref,
|
|
1152
|
+
reason: 'verify_only_on_agent_branch',
|
|
1153
|
+
verify: resolved.verify,
|
|
1154
|
+
recorded_verify: resolved.recorded,
|
|
1155
|
+
missing_path: resolved.missing,
|
|
1156
|
+
next_action: `${resolved.missing} is not on main yet; land the agent branch that has it, then the check reruns from main`,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function runResolvedVerify(resolved, workspaceRoot, cache) {
|
|
1161
|
+
const result = runVerifyCommandCached(resolved.verify, workspaceRoot, cache);
|
|
1162
|
+
if (resolved.rewritten && result.reason === 'verify_failed') {
|
|
1163
|
+
return { ...result, reason: 'verify_failed_on_main', recorded_verify: resolved.recorded };
|
|
1164
|
+
}
|
|
1165
|
+
return resolved.rewritten ? { ...result, recorded_verify: resolved.recorded } : result;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1021
1168
|
function evaluateAutoAccept(task, options = {}) {
|
|
1022
1169
|
const {
|
|
1023
1170
|
strictVerify = true,
|
|
@@ -1030,10 +1177,9 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1030
1177
|
if (task.status !== 'review') return { eligible: false, ref, reason: 'not_in_review' };
|
|
1031
1178
|
const metadata = task.metadata || {};
|
|
1032
1179
|
const review = task.review || {};
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
);
|
|
1180
|
+
// Trust is the builder's, never the approver's: a task built by a member
|
|
1181
|
+
// with a clean record lands without waiting on a human look.
|
|
1182
|
+
const trustTier = computeTrustTier(taskTrustSubject(task), task.workspace_root || process.cwd());
|
|
1037
1183
|
const tierRequiresStrictVerify = trustTier !== 'trusted';
|
|
1038
1184
|
const requiresStrictVerify = strictVerify || tierRequiresStrictVerify;
|
|
1039
1185
|
const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
|
|
@@ -1107,10 +1253,12 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1107
1253
|
}
|
|
1108
1254
|
const evidenceBlock = receiptEvidenceBlock(task, ref);
|
|
1109
1255
|
if (evidenceBlock) return evidenceBlock;
|
|
1110
|
-
const
|
|
1256
|
+
const resolvedVerify = resolveTaskVerify(task);
|
|
1257
|
+
const verify = resolvedVerify.verify;
|
|
1111
1258
|
if (tierRequiresStrictVerify && !verify) return strictVerifyMissingResult(ref);
|
|
1259
|
+
if (resolvedVerify.missing) return agentBranchOnlyResult(ref, resolvedVerify);
|
|
1112
1260
|
if (verify && executeVerify) {
|
|
1113
|
-
const verifyResult =
|
|
1261
|
+
const verifyResult = runResolvedVerify(resolvedVerify, task.workspace_root || process.cwd(), verifyCache);
|
|
1114
1262
|
// A check that runs and fails blocks. So does a check whose worktree
|
|
1115
1263
|
// is gone, otherwise the daily reap converts "has a failing check"
|
|
1116
1264
|
// into "lands unchecked" the morning after it clears the worktree.
|
|
@@ -1121,6 +1269,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1121
1269
|
// "the harness could not run the check" into "landed unchecked".
|
|
1122
1270
|
if ((tierRequiresStrictVerify && !verifyResult.ok)
|
|
1123
1271
|
|| verifyResult.reason === 'verify_failed'
|
|
1272
|
+
|| verifyResult.reason === 'verify_failed_on_main'
|
|
1124
1273
|
|| verifyResult.reason === 'verify_unrunnable'
|
|
1125
1274
|
|| verifyResult.reason === 'verify_worktree_missing'
|
|
1126
1275
|
|| verifyResult.reason === 'verify_workdir_missing') {
|
|
@@ -1206,11 +1355,13 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1206
1355
|
}
|
|
1207
1356
|
|
|
1208
1357
|
if (requiresStrictVerify) {
|
|
1209
|
-
const
|
|
1358
|
+
const resolvedVerify = resolveTaskVerify(task);
|
|
1359
|
+
const verify = resolvedVerify.verify;
|
|
1210
1360
|
if (!verify) return strictVerifyMissingResult(ref);
|
|
1361
|
+
if (resolvedVerify.missing) return agentBranchOnlyResult(ref, resolvedVerify);
|
|
1211
1362
|
if (executeVerify) {
|
|
1212
1363
|
const workspaceRoot = task.workspace_root || process.cwd();
|
|
1213
|
-
const verifyResult =
|
|
1364
|
+
const verifyResult = runResolvedVerify(resolvedVerify, workspaceRoot, verifyCache);
|
|
1214
1365
|
if (!verifyResult.ok) {
|
|
1215
1366
|
return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
|
|
1216
1367
|
}
|
|
@@ -1248,8 +1399,11 @@ module.exports = {
|
|
|
1248
1399
|
candidatePolicyGate,
|
|
1249
1400
|
declaredProtectedLane,
|
|
1250
1401
|
sniffedProtectedLane,
|
|
1402
|
+
protectedLaneOf,
|
|
1403
|
+
verifyRunnability,
|
|
1251
1404
|
evaluateAutoAccept,
|
|
1252
1405
|
isAutoCertifyVerifyCommandAllowed,
|
|
1406
|
+
parseVerifyChain,
|
|
1253
1407
|
isAgentCertified,
|
|
1254
1408
|
parseVerifyCommand,
|
|
1255
1409
|
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 };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// One plain line per cleanup run, in the daily journal's "## <project>
|
|
4
|
+
// overnight" section: the hourly autoland tick writes its own line there and
|
|
5
|
+
// so do project jobs (project-obelisk's review sweep and state sync). The
|
|
6
|
+
// session start screen reads the latest one back, so the owner sees what the
|
|
7
|
+
// background work did without opening anything.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const CLEANUP_WORDS = /\b(landed|land on|closed|put away|parked|need you|reran|archived)\b/i;
|
|
13
|
+
|
|
14
|
+
function localDate(now) {
|
|
15
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function journalFile(root, now) {
|
|
19
|
+
return path.join(root, 'atris', 'logs', String(now.getFullYear()), `${localDate(now)}.md`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sectionName(root) {
|
|
23
|
+
return `${path.basename(path.resolve(root)).replace(/^project-/, '')} overnight`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Same text twice in one day is written once.
|
|
27
|
+
function appendOvernightNote(root, line, now = new Date()) {
|
|
28
|
+
const text = String(line || '').trim();
|
|
29
|
+
if (!text || !fs.existsSync(path.join(root, 'atris'))) return false;
|
|
30
|
+
const file = journalFile(root, now);
|
|
31
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
+
const body = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
33
|
+
const bullet = `- ${text}`;
|
|
34
|
+
if (body.split('\n').some((l) => l.trim() === bullet)) return false;
|
|
35
|
+
const marker = `## ${sectionName(root)}`;
|
|
36
|
+
const idx = body.split('\n').findIndex((l) => l.trim() === marker);
|
|
37
|
+
let next;
|
|
38
|
+
if (idx === -1) {
|
|
39
|
+
next = `${body}${body && !body.endsWith('\n') ? '\n' : ''}\n${marker}\n${bullet}\n`;
|
|
40
|
+
} else {
|
|
41
|
+
const lines = body.split('\n');
|
|
42
|
+
let end = idx + 1;
|
|
43
|
+
while (end < lines.length && !lines[end].startsWith('## ')) end += 1;
|
|
44
|
+
let insertAt = end;
|
|
45
|
+
while (insertAt > idx + 1 && !lines[insertAt - 1].trim()) insertAt -= 1;
|
|
46
|
+
lines.splice(insertAt, 0, bullet);
|
|
47
|
+
next = lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
50
|
+
fs.writeFileSync(tmp, next);
|
|
51
|
+
fs.renameSync(tmp, file);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function overnightBullets(text) {
|
|
56
|
+
const bullets = [];
|
|
57
|
+
let inSection = false;
|
|
58
|
+
for (const line of String(text || '').split('\n')) {
|
|
59
|
+
if (line.startsWith('## ')) {
|
|
60
|
+
inSection = /\bovernight\s*$/i.test(line.trim());
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (inSection && line.trim().startsWith('- ')) bullets.push(line.trim().slice(2).trim());
|
|
64
|
+
}
|
|
65
|
+
return bullets;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parkedBranchCount(root) {
|
|
69
|
+
try {
|
|
70
|
+
return fs.readFileSync(path.join(root, 'atris', 'reports', 'parked-branches.md'), 'utf8')
|
|
71
|
+
.split('\n').filter((l) => /^- \d{4}-\d{2}-\d{2} · /.test(l)).length;
|
|
72
|
+
} catch {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shorten(text, max) {
|
|
78
|
+
if (text.length <= max) return text;
|
|
79
|
+
const cut = text.slice(0, max - 1);
|
|
80
|
+
const stop = Math.max(cut.lastIndexOf('; '), cut.lastIndexOf(', '));
|
|
81
|
+
return `${(stop > max * 0.5 ? cut.slice(0, stop) : cut).replace(/[\s,;.]+$/, '')}...`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The newest cleanup line from today's journal (yesterday's if today has
|
|
85
|
+
// none), plus the parked-branch count when the line does not already say it.
|
|
86
|
+
// Two small file reads; never throws.
|
|
87
|
+
function latestCleanupLine(root, { now = new Date(), max = 150 } = {}) {
|
|
88
|
+
try {
|
|
89
|
+
let line = '';
|
|
90
|
+
for (const offset of [0, 1]) {
|
|
91
|
+
const day = new Date(now);
|
|
92
|
+
day.setDate(day.getDate() - offset);
|
|
93
|
+
let text = '';
|
|
94
|
+
try { text = fs.readFileSync(journalFile(root, day), 'utf8'); } catch { continue; }
|
|
95
|
+
const bullets = overnightBullets(text).filter((b) => CLEANUP_WORDS.test(b));
|
|
96
|
+
if (bullets.length) { line = bullets[bullets.length - 1]; break; }
|
|
97
|
+
}
|
|
98
|
+
const parked = parkedBranchCount(root);
|
|
99
|
+
if (parked && !/parked/i.test(line)) {
|
|
100
|
+
const note = `${parked} agent branch${parked === 1 ? '' : 'es'} parked for you`;
|
|
101
|
+
line = line ? `${shorten(line.replace(/\.$/, ''), max - note.length - 2)}; ${note}` : `${note}.`;
|
|
102
|
+
}
|
|
103
|
+
return line ? shorten(line, max) : '';
|
|
104
|
+
} catch {
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function plural(n, word, many = `${word}s`) {
|
|
110
|
+
return `${n} ${n === 1 ? word : many}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The autoland tick's own line. Null when the tick did nothing worth a line.
|
|
114
|
+
function tickSummaryLine({ landed = 0, closed = 0, putAway = 0, needYou = 0 } = {}) {
|
|
115
|
+
const parts = [];
|
|
116
|
+
if (landed) parts.push(`landed ${plural(landed, 'finished item')}`);
|
|
117
|
+
if (closed) parts.push(`closed ${plural(closed, 'idle item')} that could never land on their own, each with a plain reason`);
|
|
118
|
+
if (putAway) parts.push(`put away ${plural(putAway, 'done or long-idle item')}`);
|
|
119
|
+
if (!parts.length) return null;
|
|
120
|
+
if (needYou) parts.push(`${needYou} need you (money, deploys, security, or customers)`);
|
|
121
|
+
const text = parts.join('; ');
|
|
122
|
+
return `Autoland: ${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
appendOvernightNote,
|
|
127
|
+
latestCleanupLine,
|
|
128
|
+
tickSummaryLine,
|
|
129
|
+
};
|
package/lib/engine-ask.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
} = require('./runner-command');
|
|
12
12
|
const {
|
|
13
13
|
canonicalEngineName,
|
|
14
|
+
currentClaudeModel,
|
|
14
15
|
engineFailureHealthStatus,
|
|
15
16
|
setEngineHealth,
|
|
16
17
|
} = require('./engine-registry');
|
|
@@ -241,7 +242,7 @@ function assertAskModelSupported(engine, model) {
|
|
|
241
242
|
|
|
242
243
|
function resolveAskModel(engine, modelName = '') {
|
|
243
244
|
const requested = String(modelName || '').trim();
|
|
244
|
-
if (requested) return requested;
|
|
245
|
+
if (requested) return currentClaudeModel(requested);
|
|
245
246
|
if (engine !== 'claude' && engine !== 'fable' && engine !== 'haiku') return '';
|
|
246
247
|
return RUNNER_PROFILE_DEFS[engine]?.model || DEFAULT_CLAUDE_RUNNER_MODEL;
|
|
247
248
|
}
|
|
@@ -291,7 +292,7 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '', {
|
|
|
291
292
|
return {
|
|
292
293
|
engine,
|
|
293
294
|
bin: profile.bin,
|
|
294
|
-
args: ['exec', ...(model ? ['-m', model] : []), ...effortArgs, '--sandbox', 'read-only', '--ephemeral', '--ignore-user-config', '--ignore-rules', '--color', 'never', request],
|
|
295
|
+
args: ['exec', ...(model ? ['-m', model] : []), ...effortArgs, '--sandbox', 'read-only', '--ephemeral', '--skip-git-repo-check', '--ignore-user-config', '--ignore-rules', '--color', 'never', request],
|
|
295
296
|
};
|
|
296
297
|
}
|
|
297
298
|
if (engine === 'cursor') {
|