atris 3.82.1 → 3.82.3
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 +1 -1
- package/commands/mission.js +3 -1
- package/commands/security-review.js +215 -8
- package/commands/task.js +6 -19
- package/commands/worktree.js +40 -9
- package/lib/campaign-keep.js +1 -0
- package/lib/first-minute.js +41 -5
- package/lib/intents.js +7 -0
- package/lib/member-context.js +20 -1
- package/lib/next-work-context.js +190 -0
- package/lib/security-audit.js +191 -12
- package/lib/sentinel-cloud.js +558 -0
- package/lib/task-db.js +2 -1
- package/lib/worktree-core.js +530 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ A computer can be personal or shared by a business. It can run a job like code,
|
|
|
18
18
|
|
|
19
19
|
Atris gives Claude Code, Codex, Cursor and any other agent a memory that lasts across sessions. The memory is plain files in your repo: `atris/MAP.md` (where everything lives, with file and line numbers), `atris/wiki/` (what the team learned, as pages linked with `[[page-name]]`), `atris/logs/` (what was done and checked each day), and `atris task` (open work, who claimed it, and the proof). Every agent reads the same files before it acts, so several agents and people share one memory, reviewed in git like code.
|
|
20
20
|
|
|
21
|
-
Mem0, Zep and Letta remember the users of a chat app. Atris remembers the project.
|
|
21
|
+
Mem0, Zep and Letta remember the users of a chat app. Atris remembers the project. Patterns and setup: [docs/agent-memory.md](docs/agent-memory.md). Comparison: https://atris.ai/blog/memory-for-coding-agents
|
|
22
22
|
|
|
23
23
|
## Check what ChatGPT can read about your business
|
|
24
24
|
|
package/commands/mission.js
CHANGED
|
@@ -7,7 +7,7 @@ const crypto = require('crypto');
|
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
const { spawn, spawnSync } = require('child_process');
|
|
9
9
|
const { hasFlag, readFlag, readIntFlag } = require('../lib/arg-parser');
|
|
10
|
-
const { memberProcessPrompt } = require('../lib/member-context');
|
|
10
|
+
const { memberProcessPrompt, currentWorkPrompt } = require('../lib/member-context');
|
|
11
11
|
const { memberIdentityLine, missionObjectiveLine } = require('../lib/member-identity');
|
|
12
12
|
const { formatToolStep, formatCodexEvent, appendProgress, reportProgress, startedLine, newRunToken, runMarker } = require('../lib/member-progress');
|
|
13
13
|
const {
|
|
@@ -8147,6 +8147,8 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8147
8147
|
...turnDownLines,
|
|
8148
8148
|
...(sharedProcess ? ['', sharedProcess] : []),
|
|
8149
8149
|
...preloadedContextLines,
|
|
8150
|
+
'',
|
|
8151
|
+
currentWorkPrompt(cwd, { ...mission, verifier: frozen.verifier }),
|
|
8150
8152
|
``,
|
|
8151
8153
|
`## Your task`,
|
|
8152
8154
|
`Do ONE increment of work toward the stop condition. ONE. No more.`,
|
|
@@ -123,10 +123,10 @@ function applyBaselineOptions(root, rawFindings, opts) {
|
|
|
123
123
|
|
|
124
124
|
function securityReviewCommand(argv = []) {
|
|
125
125
|
const sub = argv[0];
|
|
126
|
+
if (sub === 'audit') return auditCommand(argv.slice(1));
|
|
126
127
|
if (sub === 'help' || argv.includes('-h') || argv.includes('--help')) return printHelp();
|
|
127
128
|
if (sub === 'rules') return printRules();
|
|
128
129
|
if (sub === 'hook' || sub === 'install-hook') return installHook();
|
|
129
|
-
if (sub === 'audit') return auditCommand(argv.slice(1));
|
|
130
130
|
|
|
131
131
|
const root = process.cwd();
|
|
132
132
|
let opts;
|
|
@@ -402,7 +402,7 @@ function installHook() {
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
function parseAuditArgs(argv) {
|
|
405
|
-
const opts = { profile: 'standard', scope: [], json: false, planOnly: false, budget: null, out: null, hunter: null, verifier: null, timeoutMs: null };
|
|
405
|
+
const opts = { profile: 'standard', scope: [], json: false, planOnly: false, budget: null, out: null, hunter: null, verifier: null, timeoutMs: null, engine: null, yes: false, upload: true, scheduled: false };
|
|
406
406
|
const takes = (i, flag) => {
|
|
407
407
|
if (!argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(`${flag} needs a value`);
|
|
408
408
|
return argv[i + 1];
|
|
@@ -420,6 +420,10 @@ function parseAuditArgs(argv) {
|
|
|
420
420
|
else if (flag === '--hunter') opts.hunter = value();
|
|
421
421
|
else if (flag === '--verifier') opts.verifier = value();
|
|
422
422
|
else if (flag === '--timeout') opts.timeoutMs = Number(value()) * 1000;
|
|
423
|
+
else if (flag === '--engine') opts.engine = value();
|
|
424
|
+
else if (flag === '--yes' || flag === '-y') opts.yes = true;
|
|
425
|
+
else if (flag === '--no-upload') opts.upload = false;
|
|
426
|
+
else if (flag === '--scheduled') opts.scheduled = true;
|
|
423
427
|
else if (arg.startsWith('-')) throw new Error(`unknown flag: ${arg}`);
|
|
424
428
|
else opts.scope.push(arg);
|
|
425
429
|
}
|
|
@@ -427,6 +431,7 @@ function parseAuditArgs(argv) {
|
|
|
427
431
|
if (!PROFILES.includes(opts.profile)) throw new Error(`--profile must be one of ${PROFILES.join(', ')}`);
|
|
428
432
|
if (opts.budget !== null && (!Number.isInteger(opts.budget) || opts.budget < 1 || opts.budget > 500)) throw new Error('--budget must be a whole number from 1 to 500');
|
|
429
433
|
if (opts.timeoutMs !== null && (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs < 10000)) throw new Error('--timeout is in seconds, at least 10');
|
|
434
|
+
if (opts.engine !== null && !['atris', 'local'].includes(opts.engine)) throw new Error('--engine must be atris or local');
|
|
430
435
|
return opts;
|
|
431
436
|
}
|
|
432
437
|
|
|
@@ -454,7 +459,7 @@ function renderAuditLanding({ dir, meta, records }) {
|
|
|
454
459
|
} else {
|
|
455
460
|
L.push(` ${confirmed.length} confirmed${sevText ? ` (${sevText})` : ''} · ${needs.length} need a fact from you · ${rejected.length} disproved`);
|
|
456
461
|
L.push(` read ${cov.fresh} areas this run, ${cov.carried} carried unchanged, ${cov.deferred || 0} left for the next run`);
|
|
457
|
-
L.push(` found by ${meta.engines.hunter}, checked by ${meta.engines.verifier}${meta.engines.hunter === meta.engines.verifier ? ' (same engine)' : ''} · ${meta.invocations.used} engine calls of ${meta.budget}`);
|
|
462
|
+
L.push(` found by ${meta.engines.hunter}, checked by ${meta.engines.verifier}${meta.engines.hunter === meta.engines.verifier ? ' (same engine)' : ''} · ${meta.invocations.used} engine calls of ${meta.budget}${meta.credits_used ? ` · ${meta.credits_used} credits used` : ''}`);
|
|
458
463
|
for (const r of confirmed.slice(0, 6)) {
|
|
459
464
|
const sink = r.trace[r.trace.length - 1];
|
|
460
465
|
L.push(` ${r.severity.overall.padEnd(8)} ${r.title} (${sink.file}:${sink.line})`);
|
|
@@ -465,33 +470,204 @@ function renderAuditLanding({ dir, meta, records }) {
|
|
|
465
470
|
return L.join('\n');
|
|
466
471
|
}
|
|
467
472
|
|
|
468
|
-
|
|
473
|
+
// Ask once on a terminal. Anything but y or yes is a no.
|
|
474
|
+
function askYesNo(question, { input = process.stdin, output = process.stdout } = {}) {
|
|
475
|
+
return new Promise((resolve) => {
|
|
476
|
+
const rl = require('readline').createInterface({ input, output });
|
|
477
|
+
rl.question(question, (answer) => { rl.close(); resolve(/^\s*y(es)?\s*$/i.test(String(answer || ''))); });
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function uploadAfterRun(dir, { token, quiet, cloud }) {
|
|
482
|
+
const res = await cloud.uploadRun(dir, { token });
|
|
483
|
+
const say = quiet ? (m) => console.error(m) : (m) => console.log(m);
|
|
484
|
+
if (res.ok) say(' saved the summary to your atris account (no source code was sent)');
|
|
485
|
+
else say(` warning: could not save the summary to your atris account: ${res.reason}. the review itself is done`);
|
|
486
|
+
return res;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function auditUploadCommand(argv, deps) {
|
|
490
|
+
const audit = require('../lib/security-audit');
|
|
491
|
+
const cloud = deps.cloud || require('../lib/sentinel-cloud');
|
|
492
|
+
const token = deps.token !== undefined ? deps.token : cloud.authToken();
|
|
493
|
+
if (!token) { console.error(' log in first with atris login. then run atris sentinel upload again'); return 2; }
|
|
494
|
+
const arg = argv.find((a) => !a.startsWith('-'));
|
|
495
|
+
let dir = arg ? path.resolve(arg) : null;
|
|
496
|
+
if (!dir) {
|
|
497
|
+
const last = audit.latestRun(deps.root || process.cwd());
|
|
498
|
+
if (!last) { console.error(' no sentinel review has run for this repo yet'); return 2; }
|
|
499
|
+
dir = last.dir;
|
|
500
|
+
}
|
|
501
|
+
if (!fs.existsSync(path.join(dir, 'run-metadata.json'))) { console.error(` ${tildePath(dir)} is not a sentinel run folder`); return 2; }
|
|
502
|
+
const res = await cloud.uploadRun(dir, { token });
|
|
503
|
+
if (!res.ok) { console.error(` could not save ${path.basename(dir)}: ${res.reason}`); return 1; }
|
|
504
|
+
console.log(` saved ${path.basename(dir)} to your atris account (no source code was sent)`);
|
|
505
|
+
return 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Price one quick run on atris models for a folder without calling any
|
|
509
|
+
// model: the audit plans, the estimate is quoted, and the plan is dropped.
|
|
510
|
+
async function quoteQuickRun(target, { hunter, verifier, token, cloud }) {
|
|
511
|
+
const audit = require('../lib/security-audit');
|
|
512
|
+
let estimate = null;
|
|
513
|
+
await audit.runAudit({
|
|
514
|
+
root: target, profile: 'quick', hunter, verifier,
|
|
515
|
+
runJobs: async () => { throw new Error('no model call while pricing a schedule'); },
|
|
516
|
+
confirmSpend: async (est) => { estimate = est; return false; },
|
|
517
|
+
});
|
|
518
|
+
if (!estimate) throw new Error('could not plan a review for this folder');
|
|
519
|
+
return cloud.quoteEstimate(estimate, { token });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function auditScheduleCommand(argv, deps) {
|
|
523
|
+
const cloud = deps.cloud || require('../lib/sentinel-cloud');
|
|
524
|
+
const { installKeep, removeKeep } = require('../lib/campaign-keep');
|
|
525
|
+
const uninstall = argv.includes('--uninstall');
|
|
526
|
+
let every = 'day';
|
|
527
|
+
let target = null;
|
|
528
|
+
let engine = null;
|
|
529
|
+
let maxCredits = null;
|
|
530
|
+
let yes = false;
|
|
531
|
+
for (let i = 0; i < argv.length; i++) {
|
|
532
|
+
const a = argv[i];
|
|
533
|
+
const [flag, inline] = a.startsWith('--') && a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, null];
|
|
534
|
+
const value = () => (inline !== null ? inline : argv[++i]);
|
|
535
|
+
if (flag === '--every') every = value();
|
|
536
|
+
else if (flag === '--engine') engine = value();
|
|
537
|
+
else if (flag === '--max-credits') maxCredits = Number(value());
|
|
538
|
+
else if (flag === '--yes' || flag === '-y') yes = true;
|
|
539
|
+
else if (flag === '--uninstall' || flag === '--install') continue;
|
|
540
|
+
else if (a.startsWith('-')) { console.error(` unknown flag: ${a}`); return 2; }
|
|
541
|
+
else target = a;
|
|
542
|
+
}
|
|
543
|
+
if (!cloud.SCHEDULE_EVERY[every]) { console.error(' use --every day or --every week'); return 2; }
|
|
544
|
+
if (engine !== null && engine !== 'atris' && engine !== 'local') { console.error(' --engine must be atris or local'); return 2; }
|
|
545
|
+
if (maxCredits !== null && (!Number.isInteger(maxCredits) || maxCredits < 1)) { console.error(' --max-credits must be a whole number of credits, at least 1'); return 2; }
|
|
546
|
+
target = path.resolve(target || deps.root || process.cwd());
|
|
547
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) { console.error(` ${target} is not a folder`); return 2; }
|
|
548
|
+
const atrisBin = deps.atrisBin || path.resolve(__dirname, '..', 'bin', 'atris.js');
|
|
549
|
+
const plan = cloud.schedulePlan({ target, every, homeDir: deps.homeDir, platform: deps.platform, atrisBin, envPath: deps.envPath });
|
|
550
|
+
const keepDeps = deps.spawn ? { spawn: deps.spawn } : {};
|
|
551
|
+
if (uninstall) {
|
|
552
|
+
// Same folder makes the same timer name, so --every does not matter here.
|
|
553
|
+
const removed = removeKeep(plan, keepDeps);
|
|
554
|
+
if (!removed.ok) { console.error(` could not remove the timer: ${removed.error}`); return 1; }
|
|
555
|
+
cloud.removeApproval(target, deps.homeDir);
|
|
556
|
+
console.log(` scheduled reviews ${removed.removed === false ? 'were not on' : 'are off'} for ${tildePath(target)}`);
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
const space = require('../lib/worktree-space');
|
|
560
|
+
if (!deps.atrisBin && (space.isAgentCopyPath(atrisBin) || space.isScratchPath(atrisBin))) {
|
|
561
|
+
console.error(` refusing: this atris runs from ${path.dirname(path.dirname(atrisBin))}, a folder that gets removed. install the timer from a lasting atris install`);
|
|
562
|
+
return 2;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// The engine is decided now, with the owner here, and written down. A
|
|
566
|
+
// scheduled run never picks again.
|
|
567
|
+
const token = deps.token !== undefined ? deps.token : cloud.authToken();
|
|
568
|
+
let engines;
|
|
569
|
+
try { engines = cloud.resolveEngines({ engine, token, env: deps.env || process.env }); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
570
|
+
let approval;
|
|
571
|
+
if (engines.mode === 'atris') {
|
|
572
|
+
let quote;
|
|
573
|
+
try { quote = await quoteQuickRun(target, { hunter: engines.hunter, verifier: engines.verifier, token, cloud }); } catch (e) { console.error(` no timer was set. ${e.message}`); return 2; }
|
|
574
|
+
const cap = maxCredits || quote.credits;
|
|
575
|
+
console.log(`\n each scheduled review runs on atris models (no codex or claude needed)`);
|
|
576
|
+
console.log(` ${cloud.describeQuote(quote)}`);
|
|
577
|
+
console.log(` each run may spend at most ${cap} credits, once a ${every}. a run that reaches that stops\n`);
|
|
578
|
+
if (!yes) {
|
|
579
|
+
const input = deps.input || process.stdin;
|
|
580
|
+
if (!input.isTTY && !deps.input) { console.error(' no timer was set. add --yes to approve this spend without the question'); return 1; }
|
|
581
|
+
const ok = await (deps.ask || askYesNo)(` approve up to ${cap} credits per scheduled run? (y/N) `, { input, output: process.stdout });
|
|
582
|
+
if (!ok) { console.log(' no timer was set'); return 1; }
|
|
583
|
+
}
|
|
584
|
+
approval = { engine: 'atris', hunter: engines.hunter, verifier: engines.verifier, max_credits: cap, quoted_credits: quote.credits, every, approved_at: new Date().toISOString() };
|
|
585
|
+
} else {
|
|
586
|
+
approval = { engine: 'local', every, approved_at: new Date().toISOString() };
|
|
587
|
+
}
|
|
588
|
+
cloud.writeApproval(target, approval, deps.homeDir);
|
|
589
|
+
const installed = installKeep(plan, keepDeps);
|
|
590
|
+
if (!installed.ok) { cloud.removeApproval(target, deps.homeDir); console.error(` could not install the timer: ${installed.error}`); return 1; }
|
|
591
|
+
console.log(` scheduled reviews are on for ${tildePath(target)}, once a ${every}`);
|
|
592
|
+
console.log(' each run uses the quick profile and saves its summary to your atris account');
|
|
593
|
+
if (approval.engine === 'local') console.log(' runs use only codex and claude on this machine. they never switch to atris models');
|
|
594
|
+
else console.log(` runs use atris models and stop at ${approval.max_credits} credits`);
|
|
595
|
+
console.log(` log: ${tildePath(plan.log_path)}`);
|
|
596
|
+
console.log(` stop with: atris sentinel schedule --uninstall ${tildePath(target)}`);
|
|
597
|
+
return 0;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function auditCommand(argv, deps = {}) {
|
|
469
601
|
if (argv[0] === 'help' || argv.includes('-h') || argv.includes('--help')) return printAuditHelp();
|
|
470
602
|
const audit = require('../lib/security-audit');
|
|
603
|
+
const root = deps.root || process.cwd();
|
|
471
604
|
if (argv[0] === 'status' || argv[0] === 'last') {
|
|
472
|
-
const last = audit.latestRun(
|
|
605
|
+
const last = audit.latestRun(root);
|
|
473
606
|
if (!last || !last.meta) { console.log('\n no sentinel audit has run for this repo yet\n'); return 0; }
|
|
474
607
|
if (argv.includes('--json')) { console.log(JSON.stringify({ dir: last.dir, meta: last.meta, findings: last.records }, null, 2)); return 0; }
|
|
475
608
|
console.log(renderAuditLanding({ dir: last.dir, meta: last.meta, records: last.records }));
|
|
476
609
|
return 0;
|
|
477
610
|
}
|
|
611
|
+
if (argv[0] === 'upload') return auditUploadCommand(argv.slice(1), deps);
|
|
612
|
+
if (argv[0] === 'schedule') return auditScheduleCommand(argv.slice(1), deps);
|
|
478
613
|
let opts;
|
|
479
614
|
try { opts = parseAuditArgs(argv); } catch (e) { console.error(`security-review audit: ${e.message}`); return 2; }
|
|
615
|
+
const cloud = deps.cloud || require('../lib/sentinel-cloud');
|
|
616
|
+
// A timer run does only what the owner approved at install.
|
|
617
|
+
let capOverride = null;
|
|
618
|
+
if (opts.scheduled) {
|
|
619
|
+
const approval = cloud.readApproval(root, deps.homeDir);
|
|
620
|
+
if (!approval) { console.error('security-review audit: this folder has no approved schedule. nothing ran. run atris sentinel schedule again'); return 2; }
|
|
621
|
+
opts.hunter = null;
|
|
622
|
+
opts.verifier = null;
|
|
623
|
+
opts.engine = approval.engine;
|
|
624
|
+
if (approval.engine === 'atris') { opts.yes = true; capOverride = Number(approval.max_credits); }
|
|
625
|
+
}
|
|
626
|
+
const token = deps.token !== undefined ? deps.token : cloud.authToken();
|
|
627
|
+
let engines;
|
|
628
|
+
try { engines = cloud.resolveEngines({ engine: opts.engine, hunter: opts.hunter, verifier: opts.verifier, token, env: deps.env || process.env }); } catch (e) { console.error(`security-review audit: ${e.message}`); return 2; }
|
|
629
|
+
const hunter = engines.hunter || undefined;
|
|
630
|
+
const verifier = engines.verifier || undefined;
|
|
631
|
+
const hosted = audit.isHostedEngine(hunter) || audit.isHostedEngine(verifier);
|
|
632
|
+
if (hosted && !token) { console.error('security-review audit: reviews on atris models need a login; run atris login first'); return 2; }
|
|
480
633
|
const log = opts.json ? () => {} : (msg) => console.log(` · ${msg}`);
|
|
634
|
+
const tell = opts.json ? (m) => console.error(m) : (m) => console.log(m);
|
|
635
|
+
if (hosted && engines.chosen === 'no_local_engine' && !opts.planOnly) tell(' no codex or claude found on this machine. this review runs on atris models');
|
|
636
|
+
let runJobs = deps.runJobs;
|
|
637
|
+
const spend = hosted ? cloud.spendGuard({ quoteCall: deps.quoteCall }) : null;
|
|
638
|
+
if (!runJobs && hosted) runJobs = cloud.atrisJobRunner({ token, spend });
|
|
639
|
+
const confirmSpend = async (estimate) => {
|
|
640
|
+
const quote = await cloud.quoteEstimate(estimate, { token });
|
|
641
|
+
spend.max = capOverride || quote.credits;
|
|
642
|
+
spend.limits = quote.limits;
|
|
643
|
+
tell(`\n ${cloud.describeQuote(quote)}\n`);
|
|
644
|
+
if (capOverride) tell(` this scheduled run may spend at most ${capOverride} credits, as approved`);
|
|
645
|
+
if (opts.yes) return true;
|
|
646
|
+
const input = deps.input || process.stdin;
|
|
647
|
+
if (!input.isTTY && !deps.input) { console.error(' nothing was spent. add --yes to run without this question'); return false; }
|
|
648
|
+
const yes = await (deps.ask || askYesNo)(' spend these credits? (y/N) ', { input, output: opts.json ? process.stderr : process.stdout });
|
|
649
|
+
if (!yes) tell(' nothing was spent');
|
|
650
|
+
return yes;
|
|
651
|
+
};
|
|
481
652
|
let result;
|
|
482
653
|
try {
|
|
483
654
|
result = await audit.runAudit({
|
|
484
|
-
root
|
|
485
|
-
hunter
|
|
655
|
+
root, profile: opts.profile, scope: opts.scope, budget: opts.budget ?? undefined, out: opts.out,
|
|
656
|
+
hunter, verifier, planOnly: opts.planOnly, runJobs, confirmSpend,
|
|
486
657
|
timeoutMs: opts.timeoutMs || undefined, log,
|
|
487
658
|
});
|
|
488
659
|
} catch (e) {
|
|
489
660
|
console.error(`security-review audit: ${e.message}`);
|
|
490
661
|
return 2;
|
|
491
662
|
}
|
|
663
|
+
if (result.declined) return 1;
|
|
492
664
|
const fail = audit.shouldFailAudit(result.records);
|
|
493
665
|
if (opts.json) console.log(JSON.stringify({ ok: !fail, dir: result.dir, meta: result.meta, findings: result.records }, null, 2));
|
|
494
666
|
else console.log(renderAuditLanding(result));
|
|
667
|
+
if (spend && spend.blocked) tell(` stopped at the approved limit of ${spend.max} credits. ${spend.blocked} model call${spend.blocked === 1 ? '' : 's'} did not run and wait for the next run`);
|
|
668
|
+
if (opts.upload && token && !opts.planOnly) {
|
|
669
|
+
try { await uploadAfterRun(result.dir, { token, quiet: opts.json, cloud }); } catch (e) { tell(` warning: could not save the summary to your atris account: ${e.message}. the review itself is done`); }
|
|
670
|
+
}
|
|
495
671
|
return fail ? 1 : 0;
|
|
496
672
|
}
|
|
497
673
|
|
|
@@ -510,6 +686,37 @@ function printAuditHelp() {
|
|
|
510
686
|
atris security-review audit --out DIR write the run here (outside the repo)
|
|
511
687
|
atris security-review audit status the last run's landing
|
|
512
688
|
|
|
689
|
+
on atris models (no other AI subscription needed, paid in atris credits):
|
|
690
|
+
|
|
691
|
+
atris sentinel --engine atris atris:fast searches, atris:pro checks
|
|
692
|
+
atris sentinel --engine atris --yes skip the price question
|
|
693
|
+
atris sentinel --engine local only codex and claude on this machine
|
|
694
|
+
|
|
695
|
+
with no --engine, local engines run when codex or claude is installed.
|
|
696
|
+
atris models run only when neither is installed and you are logged in.
|
|
697
|
+
every run on atris models shows the most it can cost first and stops
|
|
698
|
+
before it could pass that. lines that look like secrets are hidden and
|
|
699
|
+
only tracked files inside the review are ever sent.
|
|
700
|
+
|
|
701
|
+
saving results to your atris account (on when you are logged in):
|
|
702
|
+
|
|
703
|
+
atris sentinel --no-upload keep this run on this machine only
|
|
704
|
+
atris sentinel upload [run-folder] save a past run (default: the last one)
|
|
705
|
+
|
|
706
|
+
only the summary is saved: plain sentences, file:line locations and counts.
|
|
707
|
+
source code and code lines never leave in an upload.
|
|
708
|
+
|
|
709
|
+
reviews on a timer:
|
|
710
|
+
|
|
711
|
+
atris sentinel schedule --every day [path] quick review once a day, saved to your account
|
|
712
|
+
atris sentinel schedule --every week [path] once a week
|
|
713
|
+
atris sentinel schedule --engine atris --max-credits 20
|
|
714
|
+
timed runs on atris models, at most 20 credits each
|
|
715
|
+
atris sentinel schedule --uninstall [path] stop the timer
|
|
716
|
+
|
|
717
|
+
the engine is picked when the timer goes on. on atris models you see the
|
|
718
|
+
price and say yes then. a timed run never switches to atris models later.
|
|
719
|
+
|
|
513
720
|
How it works: every source file x attack class with a reason to be read
|
|
514
721
|
becomes an area in a coverage ledger. Read-only engine hunters each own a
|
|
515
722
|
few areas. A coverage critic names gaps. Every candidate goes to a different
|
|
@@ -553,4 +760,4 @@ function printHelp() {
|
|
|
553
760
|
return 0;
|
|
554
761
|
}
|
|
555
762
|
|
|
556
|
-
module.exports = { securityReviewCommand };
|
|
763
|
+
module.exports = { securityReviewCommand, auditCommand };
|
package/commands/task.js
CHANGED
|
@@ -10,6 +10,7 @@ const { execSync } = require('node:child_process');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const os = require('os');
|
|
12
12
|
const { hasFlag } = require('../lib/arg-parser');
|
|
13
|
+
const { buildNextWorkContext, reconcileTaskRows } = require('../lib/next-work-context');
|
|
13
14
|
const { argsWantHelp } = require('../lib/noninteractive');
|
|
14
15
|
const {
|
|
15
16
|
compactErrorPayload,
|
|
@@ -74,6 +75,7 @@ const {
|
|
|
74
75
|
} = require('../lib/task-decision');
|
|
75
76
|
const {
|
|
76
77
|
buildFirstMinute,
|
|
78
|
+
loadLocalTaskState,
|
|
77
79
|
deskNextCommand,
|
|
78
80
|
firstTalkCommand,
|
|
79
81
|
folderName,
|
|
@@ -5880,27 +5882,11 @@ function cmdNextTruth(args) {
|
|
|
5880
5882
|
const taskDb = getTaskDb();
|
|
5881
5883
|
const db = taskDb.open();
|
|
5882
5884
|
const workspaceRoot = taskDb.workspaceRoot();
|
|
5883
|
-
const
|
|
5884
|
-
const existingProj = readProjectionFile(workspaceRoot);
|
|
5885
|
-
const dbHasActionable = rows.some((task) => {
|
|
5886
|
-
const status = task && task.status;
|
|
5887
|
-
return status === 'open' || status === 'claimed' || status === 'review';
|
|
5888
|
-
});
|
|
5889
|
-
const projHasActionable = Boolean(
|
|
5890
|
-
existingProj
|
|
5891
|
-
&& Array.isArray(existingProj.tasks)
|
|
5892
|
-
&& existingProj.tasks.some((task) => {
|
|
5893
|
-
const status = task && task.status;
|
|
5894
|
-
return status === 'open' || status === 'claimed' || status === 'review';
|
|
5895
|
-
}),
|
|
5896
|
-
);
|
|
5885
|
+
const snapshot = loadLocalTaskState(workspaceRoot);
|
|
5897
5886
|
let projection;
|
|
5898
5887
|
let outPath;
|
|
5899
|
-
if (
|
|
5900
|
-
projection =
|
|
5901
|
-
outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
|
|
5902
|
-
} else if (rows.length === 0 && existingProj && Array.isArray(existingProj.tasks) && existingProj.tasks.length > 0) {
|
|
5903
|
-
projection = existingProj;
|
|
5888
|
+
if (snapshot.source !== 'database') {
|
|
5889
|
+
projection = { tasks: snapshot.tasks };
|
|
5904
5890
|
outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
|
|
5905
5891
|
} else {
|
|
5906
5892
|
const written = writeDefaultProjection(taskDb, db);
|
|
@@ -5928,6 +5914,7 @@ function cmdNextTruth(args) {
|
|
|
5928
5914
|
scope: normalizeTaskQueueScope(scope),
|
|
5929
5915
|
projection_path: outPath,
|
|
5930
5916
|
task: task ? compactTaskFromProjection(projection, task.id) : null,
|
|
5917
|
+
context: buildNextWorkContext({ root: workspaceRoot, tasks: reconcileTaskRows(snapshot.tasks, projection.tasks || []), selected: task, owner: String(owner), taskSource: snapshot.source, generatedAt: snapshot.generated_at }),
|
|
5931
5918
|
});
|
|
5932
5919
|
return;
|
|
5933
5920
|
}
|
package/commands/worktree.js
CHANGED
|
@@ -12,10 +12,12 @@ const worktreeCore = require('../lib/worktree-core');
|
|
|
12
12
|
|
|
13
13
|
const {
|
|
14
14
|
ATRIS_BIN,
|
|
15
|
+
RESCUE_IDLE_MS,
|
|
16
|
+
finishShippedCopy,
|
|
17
|
+
formatKib,
|
|
15
18
|
SWEEP_IDLE_MS,
|
|
16
19
|
SWEEP_EVERY_HOURS,
|
|
17
20
|
SWEEP_KEPT_LABELS,
|
|
18
|
-
announceShippedCopyRemoval,
|
|
19
21
|
assertOneLapProofTree,
|
|
20
22
|
baseBranchName,
|
|
21
23
|
branchName,
|
|
@@ -50,6 +52,7 @@ const {
|
|
|
50
52
|
prMergeRef,
|
|
51
53
|
protectedPushRefusal,
|
|
52
54
|
readOneLapProofBinding,
|
|
55
|
+
reapPushedCopy,
|
|
53
56
|
reapShippedCopy,
|
|
54
57
|
recordWorktreeRemoval,
|
|
55
58
|
refreshMovedMapRefs,
|
|
@@ -190,6 +193,7 @@ function shipHelp() {
|
|
|
190
193
|
console.log(' protected-lane tasks (billing, money, deploy, feedback, ...) can be pushed for review without --merge;');
|
|
191
194
|
console.log(' landing one with --merge or --local needs --allow-denied-lane on written order');
|
|
192
195
|
console.log(' --no-map-refresh skip the automatic refresh of moved map line references');
|
|
196
|
+
console.log(' --keep-copy keep this build copy; by default it is removed once the work lands or is pushed');
|
|
193
197
|
console.log(' unstaged regenerated adapter files are skipped unless staged first or named in --message');
|
|
194
198
|
console.log(' recommended flight verify: npm run test:fast && node --test <focused files>');
|
|
195
199
|
}
|
|
@@ -204,6 +208,7 @@ function shipWorktree(args) {
|
|
|
204
208
|
const noPr = hasFlag(args, '--no-pr');
|
|
205
209
|
const localMode = hasFlag(args, '--local') || hasFlag(args, '--local-merge');
|
|
206
210
|
const merge = hasFlag(args, '--merge');
|
|
211
|
+
const keepCopy = hasFlag(args, '--keep-copy');
|
|
207
212
|
const message = readFlag(args, '--message') || readFlag(args, '-m');
|
|
208
213
|
const verify = readFlag(args, '--verify');
|
|
209
214
|
let branch = currentBranch(root);
|
|
@@ -351,7 +356,7 @@ function shipWorktree(args) {
|
|
|
351
356
|
}
|
|
352
357
|
console.log('pr: skipped (local mode)');
|
|
353
358
|
if (merge && !dryRun) runShipHealthCheck(primary || root);
|
|
354
|
-
if (landedOn)
|
|
359
|
+
if (landedOn) finishShippedCopy({ root, primary, branch, landedOn, keep: keepCopy });
|
|
355
360
|
console.log('done: worktree shipped');
|
|
356
361
|
return 0;
|
|
357
362
|
}
|
|
@@ -385,6 +390,7 @@ function shipWorktree(args) {
|
|
|
385
390
|
}
|
|
386
391
|
if (state.state === 'OPEN' && !state.mergedAt) {
|
|
387
392
|
console.log('merge: pending; kept the source branch while required checks finish');
|
|
393
|
+
finishShippedCopy({ root, primary, branch, keep: keepCopy });
|
|
388
394
|
return 0;
|
|
389
395
|
}
|
|
390
396
|
if (state.state !== 'MERGED' || !state.mergedAt) {
|
|
@@ -414,7 +420,7 @@ function shipWorktree(args) {
|
|
|
414
420
|
}, { worktree: root });
|
|
415
421
|
} catch {}
|
|
416
422
|
if (merge && !dryRun) runShipHealthCheck(primary || root);
|
|
417
|
-
if (
|
|
423
|
+
if (!dryRun) finishShippedCopy({ root, primary, branch, landedOn, keep: keepCopy });
|
|
418
424
|
console.log('done: worktree shipped');
|
|
419
425
|
return 0;
|
|
420
426
|
}
|
|
@@ -425,7 +431,10 @@ function reapShipped() {
|
|
|
425
431
|
console.error('reap-shipped: ATRIS_REAP_WORKTREE is not set');
|
|
426
432
|
return 2;
|
|
427
433
|
}
|
|
428
|
-
const
|
|
434
|
+
const base = process.env.ATRIS_REAP_BASE || '';
|
|
435
|
+
const result = base
|
|
436
|
+
? reapShippedCopy({ root: process.cwd(), target, base })
|
|
437
|
+
: reapPushedCopy({ primary: process.cwd(), target, branch: process.env.ATRIS_REAP_BRANCH || '' });
|
|
429
438
|
console.log(`reap-shipped: ${result.removed ? 'removed' : 'kept'} ${target} (${result.reason})`);
|
|
430
439
|
return result.removed ? 0 : 1;
|
|
431
440
|
}
|
|
@@ -495,8 +504,14 @@ function sweep(args) {
|
|
|
495
504
|
console.error('Usage: atris worktree sweep [--apply] [--json] [--workspace <dir>] [--idle-hours <n>]');
|
|
496
505
|
return 2;
|
|
497
506
|
}
|
|
507
|
+
const rescueDays = Number(readFlag(args, '--rescue-after-days', process.env.ATRIS_WORKTREE_RESCUE_DAYS || String(RESCUE_IDLE_MS / 86400000)));
|
|
508
|
+
if (!Number.isFinite(rescueDays) || rescueDays <= 0) {
|
|
509
|
+
console.error('Usage: atris worktree sweep [--apply] [--json] [--workspace <dir>] [--idle-hours <n>] [--rescue] [--rescue-after-days <n>]');
|
|
510
|
+
return 2;
|
|
511
|
+
}
|
|
498
512
|
const apply = hasFlag(args, '--apply');
|
|
499
|
-
const
|
|
513
|
+
const rescue = hasFlag(args, '--rescue');
|
|
514
|
+
const report = sweepWorkspace({ workspace, apply, rescue, idleMs: idleHours * 3600000, rescueIdleMs: rescueDays * 86400000 });
|
|
500
515
|
if (hasFlag(args, '--json')) {
|
|
501
516
|
console.log(JSON.stringify({ ok: !report.error, ...report }, null, 2));
|
|
502
517
|
return report.error ? 1 : 0;
|
|
@@ -514,9 +529,11 @@ function sweep(args) {
|
|
|
514
529
|
console.log(`${repo.name}: skipped (${repo.error || repo.skipped})`);
|
|
515
530
|
continue;
|
|
516
531
|
}
|
|
517
|
-
const
|
|
532
|
+
const saved = apply ? repo.rescued : repo.rescueCandidates;
|
|
533
|
+
const savedSet = new Set(saved);
|
|
534
|
+
const gone = (apply ? repo.removed : repo.candidates).filter((item) => !savedSet.has(item));
|
|
518
535
|
const cleared = apply ? repo.cachePruned : repo.cacheCandidates.filter((item) => item.reason === 'generated_webpack_cache');
|
|
519
|
-
removedTotal += gone.length;
|
|
536
|
+
removedTotal += gone.length + saved.length;
|
|
520
537
|
clearedTotal += cleared.length;
|
|
521
538
|
reviewTotal += repo.cacheCandidates.filter((item) => item.reason === 'manual_review_required').length;
|
|
522
539
|
const counts = {};
|
|
@@ -526,7 +543,18 @@ function sweep(args) {
|
|
|
526
543
|
}
|
|
527
544
|
const keptText = Object.entries(counts).map(([label, count]) => `${count} ${label}`).join(', ');
|
|
528
545
|
console.log(`${repo.name}: ${apply ? 'removed' : 'would remove'} ${plural(gone.length, 'finished copy', 'finished copies')}, ${apply ? 'cleared' : 'would clear'} ${plural(cleared.length, 'webpack cache', 'webpack caches')}; kept ${repo.kept.length}${keptText ? ` (${keptText})` : ''}`);
|
|
546
|
+
if (saved.length) console.log(` ${apply ? 'saved and removed' : 'would save and remove'} ${plural(saved.length, 'abandoned copy', 'abandoned copies')}`);
|
|
547
|
+
for (const item of saved) {
|
|
548
|
+
const what = item.reason === 'pushed_branch' ? 'already on origin' : `${plural(item.changed_files || 0, 'changed file', 'changed files')}, idle ${item.idle_days} days`;
|
|
549
|
+
console.log(` ${apply ? 'saved' : 'save'}: ${item.path} -> origin/${item.rescue_branch} (${what}, ${formatKib(item.size_kib)})`);
|
|
550
|
+
}
|
|
529
551
|
for (const item of gone) console.log(` ${apply ? 'removed' : 'remove'}: ${item.path}`);
|
|
552
|
+
for (const item of repo.kept) {
|
|
553
|
+
if (item.reason === 'rescue_push_failed') console.log(` kept: ${item.path} (could not push its work to origin/${item.rescue_branch}: ${item.error})`);
|
|
554
|
+
if (item.reason === 'unprovable') console.log(` kept: ${item.path} (${item.why})`);
|
|
555
|
+
if (item.reason === 'stale_dirty' || item.reason === 'stale_unpushed') console.log(` stale: ${item.path} (idle ${item.idle_days} days; atris worktree sweep --rescue --apply saves and removes it)`);
|
|
556
|
+
if (item.reason === 'ignored_data' && item.files) console.log(` kept: ${item.path} (ignored files atris cannot rebuild: ${item.files.join(', ')})`);
|
|
557
|
+
}
|
|
530
558
|
for (const item of cleared) console.log(` ${apply ? 'cleared' : 'clear'}: ${item.path}`);
|
|
531
559
|
}
|
|
532
560
|
if (reviewTotal) console.log(`review: ${plural(reviewTotal, 'other build folder waits', 'other build folders wait')} for a person; \`atris worktree cleanup\` in each repo lists them`);
|
|
@@ -603,7 +631,8 @@ function guide() {
|
|
|
603
631
|
console.log('Start reaps completed checkouts first. Cleanup preserves branches, source changes, and files git ignores, skips live work, and prunes only verified webpack caches.');
|
|
604
632
|
console.log('Start clones node_modules from a matching install when package-lock.json matches; skip npm ci when it prints "deps: cloned".');
|
|
605
633
|
console.log('Below 25 GB free, start reaps finished copies in every repo; below 10 GB it refuses (ATRIS_WORKTREE_REAP_BELOW_GB, ATRIS_WORKTREE_MIN_FREE_GB).');
|
|
606
|
-
console.log('A copy
|
|
634
|
+
console.log('A shipped copy is removed as soon as its work is merged or on origin and nothing else runs in it; if something does, a merged copy goes once it has been quiet for 10 minutes (--keep-copy or ATRIS_WORKTREE_KEEP_AFTER_SHIP=1 keeps it).');
|
|
635
|
+
console.log('The sweep reports abandoned copies with unsaved work (idle 7 days). With --rescue it saves one to commits on a new atris-rescue/ branch, pushes it, and removes the copy only after origin has it; it removes a copy only when it can prove every byte is saved, and anything unusual keeps the copy for a person. The timer never rescues. Rescues are logged in .atris/state/worktree_removals.jsonl.');
|
|
607
636
|
console.log('Use `atris worktree status` to see all worktrees and dirty counts.');
|
|
608
637
|
return 0;
|
|
609
638
|
}
|
|
@@ -620,12 +649,14 @@ function help() {
|
|
|
620
649
|
console.log(' --target <ref> override the default landing target (default: branch atris-base, else origin default branch)');
|
|
621
650
|
console.log(' --local merge into the local primary checkout instead of pushing and opening a PR');
|
|
622
651
|
console.log(' --no-map-refresh skip the automatic refresh of moved map line references');
|
|
652
|
+
console.log(' --keep-copy keep this build copy after it ships');
|
|
623
653
|
console.log(' recommended verify: npm run test:fast && node --test <focused files>');
|
|
624
654
|
console.log(' atris worktree status');
|
|
625
655
|
console.log(' atris worktree guard [--allow-primary] [--allow-dirty]');
|
|
626
656
|
console.log(' atris worktree prune [--apply]');
|
|
627
657
|
console.log(' atris worktree cleanup [--apply] [--json] [--base origin/master] reap completed checkouts and verified webpack caches');
|
|
628
|
-
console.log(' atris worktree sweep [--apply] [--json] [--workspace <dir>] [--idle-hours 2]
|
|
658
|
+
console.log(' atris worktree sweep [--apply] [--json] [--workspace <dir>] [--idle-hours 2] [--rescue] [--rescue-after-days 7]');
|
|
659
|
+
console.log(' removes merged or pushed idle copies in every repo and reports abandoned ones; --rescue saves those to a new branch on origin, then removes them');
|
|
629
660
|
console.log(' atris worktree sweep --install [--every 3] | --uninstall run the sweep on a timer');
|
|
630
661
|
}
|
|
631
662
|
|
package/lib/campaign-keep.js
CHANGED
|
@@ -35,6 +35,7 @@ function cronSchedule(seconds) {
|
|
|
35
35
|
if (minutes === 1) return '* * * * *';
|
|
36
36
|
if (minutes < 60) return `*/${minutes} * * * *`;
|
|
37
37
|
const hours = Math.round(minutes / 60);
|
|
38
|
+
if (hours >= 168) return '0 0 * * 0';
|
|
38
39
|
return hours >= 24 ? '0 0 * * *' : `0 */${hours} * * *`;
|
|
39
40
|
}
|
|
40
41
|
|
package/lib/first-minute.js
CHANGED
|
@@ -8,6 +8,7 @@ const { spawnSync } = require('child_process');
|
|
|
8
8
|
const { isGenericScratchRoot } = require('./scratch-root');
|
|
9
9
|
const { resolveDefaultVerifier } = require('./default-verifier');
|
|
10
10
|
const { nonFalsifyingIssue } = require('./accept-verify-gate');
|
|
11
|
+
const { reconcileTaskRows } = require('./next-work-context');
|
|
11
12
|
|
|
12
13
|
const FIRST_USE_COMMAND = 'atris "help me choose the first useful step for this project"';
|
|
13
14
|
const FIRST_TALK_ASK = 'what do you want here';
|
|
@@ -176,8 +177,10 @@ function titlesFromTodoSection(root, section) {
|
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
function
|
|
180
|
+
function loadLocalTaskState(root = process.cwd(), { taskIds = [] } = {}) {
|
|
180
181
|
let dbRows = [];
|
|
182
|
+
let lookup = () => null;
|
|
183
|
+
let databaseRead = false;
|
|
181
184
|
try {
|
|
182
185
|
const taskDb = require('./task-db');
|
|
183
186
|
// Reading never creates the db, so a first look writes nothing.
|
|
@@ -185,20 +188,52 @@ function loadLocalTasks(root = process.cwd()) {
|
|
|
185
188
|
const db = taskDb.open();
|
|
186
189
|
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
187
190
|
const rows = taskDb.listTasks(db, { workspaceRoot, limit: 200 });
|
|
188
|
-
|
|
191
|
+
// The visible window must not become a new numbering universe. Task
|
|
192
|
+
// commands resolve these references against the complete workspace.
|
|
193
|
+
const refRows = taskDb.listTasks(db, { workspaceRoot });
|
|
194
|
+
// Busy queues must not crowd the last outcomes out of the handoff.
|
|
195
|
+
const recent = ['done', 'failed'].flatMap((status) => taskDb.listTasks(db, { workspaceRoot, status, limit: 3, orderBy: 'updated_at' }));
|
|
196
|
+
const seen = new Set(rows.map((row) => row.id));
|
|
197
|
+
rows.push(...recent.filter((row) => !seen.has(row.id)));
|
|
198
|
+
if (Array.isArray(rows) && rows.length) dbRows = taskDb.withTaskDisplayRefs(rows, refRows);
|
|
199
|
+
databaseRead = true;
|
|
200
|
+
lookup = (id) => {
|
|
201
|
+
const row = taskDb.getTask(db, id);
|
|
202
|
+
// A foreign id in a damaged projection is not this workspace's work.
|
|
203
|
+
if (row && !taskDb.rowInWorkspace(row, workspaceRoot)) return false;
|
|
204
|
+
return row ? taskDb.withTaskDisplayRefs([row], refRows)[0] : null;
|
|
205
|
+
};
|
|
206
|
+
// A shortlist is an output budget, not authority to invent a bound job.
|
|
207
|
+
for (const id of taskIds.slice(0, 8)) {
|
|
208
|
+
if (dbRows.some((row) => row.id === id)) continue;
|
|
209
|
+
const row = lookup(id);
|
|
210
|
+
if (row) dbRows.push(row);
|
|
211
|
+
}
|
|
189
212
|
} catch {
|
|
190
213
|
// Fall through to the local projection. Tests and fresh folders often have no db.
|
|
191
214
|
}
|
|
192
|
-
if (dbRows.some(isActionableTask)) return dbRows;
|
|
215
|
+
if (dbRows.some(isActionableTask)) return { tasks: dbRows, source: 'database' };
|
|
193
216
|
const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
194
217
|
try {
|
|
195
218
|
const parsed = JSON.parse(fs.readFileSync(projectionPath, 'utf8'));
|
|
219
|
+
if (parsed.workspace_root && path.resolve(parsed.workspace_root) !== path.resolve(require('./task-db').workspaceRoot(root))) {
|
|
220
|
+
return { tasks: dbRows, source: databaseRead ? 'database' : 'none' };
|
|
221
|
+
}
|
|
196
222
|
const projected = Array.isArray(parsed.tasks) ? parsed.tasks : [];
|
|
197
|
-
if (projected.some(isActionableTask) || !dbRows.length)
|
|
223
|
+
if (projected.some(isActionableTask) || !dbRows.length) {
|
|
224
|
+
const tasks = reconcileTaskRows(dbRows, projected, lookup);
|
|
225
|
+
const known = new Set(dbRows.map((row) => row.id));
|
|
226
|
+
const unknown = tasks.some((row) => !row.id || (!known.has(row.id) && !lookup(row.id)));
|
|
227
|
+
return { tasks, source: unknown || !databaseRead ? (dbRows.length ? 'mixed' : 'projection') : 'database', generated_at: parsed.generated_at || null };
|
|
228
|
+
}
|
|
198
229
|
} catch {
|
|
199
230
|
// Keep any done-only db rows below.
|
|
200
231
|
}
|
|
201
|
-
return dbRows;
|
|
232
|
+
return { tasks: dbRows, source: databaseRead ? 'database' : 'none' };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function loadLocalTasks(root = process.cwd()) {
|
|
236
|
+
return loadLocalTaskState(root).tasks;
|
|
202
237
|
}
|
|
203
238
|
|
|
204
239
|
function loadLatestRecap(root = process.cwd()) {
|
|
@@ -1170,6 +1205,7 @@ module.exports = {
|
|
|
1170
1205
|
spokenBuiltList,
|
|
1171
1206
|
taskStartedAt,
|
|
1172
1207
|
loadLocalTasks,
|
|
1208
|
+
loadLocalTaskState,
|
|
1173
1209
|
firstTalkBuildCommand,
|
|
1174
1210
|
renderWhatsNextAsk,
|
|
1175
1211
|
isLeftoverVerbLook,
|
package/lib/intents.js
CHANGED
|
@@ -8,6 +8,13 @@ const INTENTS = Object.freeze([
|
|
|
8
8
|
plain: 'your agent picks the most useful next step and starts it.',
|
|
9
9
|
offer: 'boot_empty',
|
|
10
10
|
}),
|
|
11
|
+
Object.freeze({
|
|
12
|
+
id: 'prioritize_work',
|
|
13
|
+
say: Object.freeze(['whats the most important work to work on', 'what should we work on next', 'what is the highest priority work', 'help me choose what to work on']),
|
|
14
|
+
do: 'atris task next --json',
|
|
15
|
+
plain: 'your agent reads the current goal, existing parts and recent evidence before recommending the next work.',
|
|
16
|
+
offer: 'always',
|
|
17
|
+
}),
|
|
11
18
|
Object.freeze({
|
|
12
19
|
id: 'keep_going',
|
|
13
20
|
say: Object.freeze(['keep going while i\'m away', 'run overnight', 'finish this', 'keep working without me']),
|