atris 3.68.1 → 3.68.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/atris/skills/engines/SKILL.md +1 -1
- package/atris/team/customer-lead/MEMBER.md +1 -1
- package/atris/team/executor/MEMBER.md +19 -18
- package/atris/team/executor/skills/isolated-slice-builder/SKILL.md +21 -0
- package/atris/team/navigator/MEMBER.md +3 -1
- package/atris/team/navigator/skills/loophole-proof-planner/SKILL.md +20 -0
- package/atris/team/researcher/MEMBER.md +2 -1
- package/atris/team/researcher/skills/grounded-evidence-brief/SKILL.md +19 -0
- package/atris/team/validator/MEMBER.md +2 -7
- package/atris/team/validator/skills/cross-model-referee/SKILL.md +20 -0
- package/bin/atris.js +40 -1
- package/commands/autoland.js +2 -1
- package/commands/done.js +374 -0
- package/commands/engine.js +2 -2
- package/commands/goal.js +61 -2
- package/commands/land.js +34 -7
- package/commands/member.js +1542 -41
- package/commands/mission.js +506 -18
- package/commands/task.js +26 -3
- package/commands/workflow.js +1 -1
- package/commands/worktree.js +144 -34
- package/lib/auto-accept-certified.js +7 -6
- package/lib/codex-flight.js +6 -12
- package/lib/conductor-artifacts.js +3 -1
- package/lib/default-runner.js +42 -7
- package/lib/engine-ask.js +8 -1
- package/lib/engine-registry.js +17 -0
- package/lib/falsifier-probe.js +6 -0
- package/lib/first-minute.js +257 -6
- package/lib/fleet.js +163 -88
- package/lib/known-commands.js +1 -1
- package/lib/member-alive.js +1 -1
- package/lib/member-approval.js +153 -0
- package/lib/mission-task-gate.js +170 -0
- package/lib/one-lap-validator.js +2 -1
- package/lib/roster-runs.js +28 -0
- package/lib/runner-command.js +32 -8
- package/lib/task-db.js +25 -1
- package/package.json +9 -2
- package/scripts/det/ytnotes +49 -10
- package/utils/backend-root.js +4 -0
package/lib/first-minute.js
CHANGED
|
@@ -272,6 +272,25 @@ function taskNextCommand(tasks, person) {
|
|
|
272
272
|
return picked.command;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
// A person's first talk mints an onboarding task in their own words. That task
|
|
276
|
+
// is the human's thing; the init seed ("Generate MAP.md, ...") is agent setup.
|
|
277
|
+
function isHumanStarter(task) {
|
|
278
|
+
const metadata = (task && task.metadata) || {};
|
|
279
|
+
return Boolean(task) && (metadata.source === 'context_gatherer' || task.tag === 'onboarding');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isInitSeedTask(task) {
|
|
283
|
+
return Boolean(task && task.metadata && task.metadata.source === 'init_seed');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// In a room a person started by talking, the open init seed is not their next
|
|
287
|
+
// step. Agents still find it on the task plane; the human screens skip it.
|
|
288
|
+
function withoutSeedForHumans(tasks) {
|
|
289
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
290
|
+
if (!list.some(isHumanStarter)) return list;
|
|
291
|
+
return list.filter((task) => !(isInitSeedTask(task) && task.status === 'open'));
|
|
292
|
+
}
|
|
293
|
+
|
|
275
294
|
function pickNext({
|
|
276
295
|
tasks = [],
|
|
277
296
|
missions = [],
|
|
@@ -281,6 +300,7 @@ function pickNext({
|
|
|
281
300
|
inboxCount = 0,
|
|
282
301
|
wipCount = 0,
|
|
283
302
|
} = {}) {
|
|
303
|
+
tasks = withoutSeedForHumans(tasks);
|
|
284
304
|
const reviews = (Array.isArray(tasks) ? tasks : []).filter((task) => task && task.status === 'review');
|
|
285
305
|
const certified = newest(reviews.filter(isCertifiedReview));
|
|
286
306
|
if (certified) return { task: certified, command: taskCommand(certified, person) };
|
|
@@ -361,6 +381,94 @@ function spokenVisibleNames(files) {
|
|
|
361
381
|
return names.sort((a, b) => a.localeCompare(b));
|
|
362
382
|
}
|
|
363
383
|
|
|
384
|
+
const BUILT_SKIP_DIRS = new Set(['atris', 'node_modules']);
|
|
385
|
+
const BUILT_MAX_FILES = 500;
|
|
386
|
+
const BUILT_MAX_DEPTH = 6;
|
|
387
|
+
|
|
388
|
+
function toMs(value) {
|
|
389
|
+
if (value == null || value === '') return 0;
|
|
390
|
+
const n = Number(value);
|
|
391
|
+
if (Number.isFinite(n)) return n;
|
|
392
|
+
const parsed = Date.parse(String(value));
|
|
393
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function taskStartedAt(task) {
|
|
397
|
+
return toMs(task && task.claimed_at) || toMs(task && task.created_at);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Files a person can see that were written after `sinceMs`: what their coding
|
|
401
|
+
// agent built. atris/, .atris/, node_modules, and dotfiles are not the work.
|
|
402
|
+
function listBuiltSince(root = process.cwd(), sinceMs = 0) {
|
|
403
|
+
const base = path.resolve(root || '.');
|
|
404
|
+
const since = Number(sinceMs) || 0;
|
|
405
|
+
const found = [];
|
|
406
|
+
let seen = 0;
|
|
407
|
+
const walk = (dir, rel, depth) => {
|
|
408
|
+
if (depth > BUILT_MAX_DEPTH || seen >= BUILT_MAX_FILES) return;
|
|
409
|
+
let entries = [];
|
|
410
|
+
try {
|
|
411
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
412
|
+
} catch {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
for (const entry of entries) {
|
|
416
|
+
if (seen >= BUILT_MAX_FILES) return;
|
|
417
|
+
const name = entry.name;
|
|
418
|
+
if (!isUserVisibleName(name) || name === LATER_NOTE_FILE) continue;
|
|
419
|
+
if (depth === 0 && BUILT_SKIP_DIRS.has(name)) continue;
|
|
420
|
+
if (name === 'node_modules') continue;
|
|
421
|
+
const full = path.join(dir, name);
|
|
422
|
+
const relPath = rel ? `${rel}/${name}` : name;
|
|
423
|
+
if (entry.isDirectory()) {
|
|
424
|
+
walk(full, relPath, depth + 1);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (!entry.isFile()) continue;
|
|
428
|
+
seen += 1;
|
|
429
|
+
let stat;
|
|
430
|
+
try {
|
|
431
|
+
stat = fs.statSync(full);
|
|
432
|
+
} catch {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (stat.mtimeMs >= since) found.push(relPath);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
walk(base, '', 0);
|
|
439
|
+
return found.sort((a, b) => a.localeCompare(b));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// The thing to open: an index.html (shallowest first), else the first file.
|
|
443
|
+
function mainBuiltFile(files) {
|
|
444
|
+
const list = Array.isArray(files) ? files : [];
|
|
445
|
+
const pages = list
|
|
446
|
+
.filter((file) => /(^|\/)index\.html?$/i.test(file))
|
|
447
|
+
.sort((a, b) => a.split('/').length - b.split('/').length || a.localeCompare(b));
|
|
448
|
+
return pages[0] || list[0] || '';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function orderBuilt(files) {
|
|
452
|
+
const list = Array.isArray(files) ? [...files] : [];
|
|
453
|
+
const main = mainBuiltFile(list);
|
|
454
|
+
if (!main) return list;
|
|
455
|
+
return [main, ...list.filter((file) => file !== main)];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function spokenBuiltList(files, max = 3) {
|
|
459
|
+
const list = orderBuilt(files);
|
|
460
|
+
if (list.length <= max) return list.join(', ');
|
|
461
|
+
return `${list.slice(0, max).join(', ')} and ${list.length - max} more`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function humanStarterTask(tasks, person = '', { statuses = ['claimed', 'review'] } = {}) {
|
|
465
|
+
const list = (Array.isArray(tasks) ? tasks : [])
|
|
466
|
+
.filter((task) => isHumanStarter(task) && statuses.includes(task.status));
|
|
467
|
+
const who = String(person || '').trim();
|
|
468
|
+
const mine = who ? list.filter((task) => String(task.claimed_by || '') === who) : [];
|
|
469
|
+
return newest(mine.length ? mine : list);
|
|
470
|
+
}
|
|
471
|
+
|
|
364
472
|
function spokenCommitSubject(value) {
|
|
365
473
|
const text = clip(value, 72);
|
|
366
474
|
if (text.endsWith('...')) return text;
|
|
@@ -619,13 +727,85 @@ function firstTalkJson({ starter = null, person = '', folder = 'this folder' } =
|
|
|
619
727
|
};
|
|
620
728
|
}
|
|
621
729
|
|
|
622
|
-
|
|
730
|
+
// First talk only saves the want as a task. Say so, and make the one next step
|
|
731
|
+
// the local coding agent that builds it, so a new person is never told work
|
|
732
|
+
// exists when none does. Test runners get no probe (machine-independent output).
|
|
733
|
+
const FIRST_TALK_BUILDERS = ['claude', 'codex', 'cursor-agent'];
|
|
734
|
+
const FIRST_TALK_INSTALL = 'npm install -g @anthropic-ai/claude-code';
|
|
735
|
+
|
|
736
|
+
function firstTalkBuilder(probe = (bin) => spawnSync('sh', ['-c', `command -v ${bin}`], { encoding: 'utf8' })) {
|
|
737
|
+
if (process.env.NODE_TEST_CONTEXT && arguments.length === 0) return undefined;
|
|
738
|
+
for (const bin of FIRST_TALK_BUILDERS) {
|
|
739
|
+
const result = probe(bin);
|
|
740
|
+
if (result && result.status === 0 && String(result.stdout || '').trim()) return bin;
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function firstTalkBuildCommand(builder, want) {
|
|
746
|
+
if (builder === undefined) return '';
|
|
747
|
+
if (!builder) return FIRST_TALK_INSTALL;
|
|
748
|
+
const text = String(want || '').replace(/\s+/g, ' ').trim().replace(/(["\\$`])/g, '\\$1');
|
|
749
|
+
return `${builder} "${text}"`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function renderFirstTalk({ person = '', folder = 'this folder', starter = null, builder } = {}) {
|
|
623
753
|
const room = folder || 'this folder';
|
|
624
754
|
const title = spokenStarterTitle(starter && starter.title, room);
|
|
755
|
+
const build = firstTalkBuildCommand(builder, (starter && starter.title) || title);
|
|
625
756
|
return [
|
|
626
|
-
`${greet(person)}${title} is
|
|
757
|
+
`${greet(person)}${title} is started. nothing is built yet.`,
|
|
627
758
|
'',
|
|
628
|
-
`next: ${firstTalkNext({ starter, person, folder: room })}`,
|
|
759
|
+
`next: ${build || firstTalkNext({ starter, person, folder: room })}`,
|
|
760
|
+
].join('\n');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const WHATS_NEXT_COMMAND = 'atris "what\'s next?"';
|
|
764
|
+
|
|
765
|
+
function isWhatsNextLine(value) {
|
|
766
|
+
const text = String(value || '')
|
|
767
|
+
.trim()
|
|
768
|
+
.replace(/^atris\s+/i, '')
|
|
769
|
+
.replace(/^["']+|["']+$/g, '')
|
|
770
|
+
.toLowerCase()
|
|
771
|
+
.replace(/[?.!]+$/g, '')
|
|
772
|
+
.replace(/\s+/g, ' ')
|
|
773
|
+
.trim();
|
|
774
|
+
return /^(what'?s|what is) next$/.test(text) || text === 'what next';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function starterTitle(task, folder = 'this folder') {
|
|
778
|
+
return spokenStarterTitle(clip(task && task.title, 80), folder);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function renderStarterNewWork({ person = '', task = null, files = [], folder = 'this folder' } = {}) {
|
|
782
|
+
return [
|
|
783
|
+
`${greet(person)}${starterTitle(task, folder)} has new work: ${spokenBuiltList(files)}.`,
|
|
784
|
+
'',
|
|
785
|
+
'next: atris done',
|
|
786
|
+
].join('\n');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// The human's first want is landed and nothing else of theirs is live.
|
|
790
|
+
function shippedHumanStarter(tasks) {
|
|
791
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
792
|
+
if (list.some((task) => isHumanStarter(task) && isActionableTask(task))) return null;
|
|
793
|
+
return newest(list.filter((task) => isHumanStarter(task) && task.status === 'done'));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function renderStarterShipped({ person = '', task = null, folder = 'this folder' } = {}) {
|
|
797
|
+
return [
|
|
798
|
+
`${greet(person)}you already shipped ${starterTitle(task, folder)}.`,
|
|
799
|
+
'',
|
|
800
|
+
`next: ${WHATS_NEXT_COMMAND}`,
|
|
801
|
+
].join('\n');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function renderWhatsNextAsk({ person = '', task = null, folder = 'this folder' } = {}) {
|
|
805
|
+
return [
|
|
806
|
+
`${greet(person)}${starterTitle(task, folder)} is shipped. say the next thing you want here, in your own words.`,
|
|
807
|
+
'',
|
|
808
|
+
'next: atris "<the next thing you want>"',
|
|
629
809
|
].join('\n');
|
|
630
810
|
}
|
|
631
811
|
|
|
@@ -694,6 +874,7 @@ function buildFirstMinute({
|
|
|
694
874
|
dirty,
|
|
695
875
|
branch,
|
|
696
876
|
later,
|
|
877
|
+
builder,
|
|
697
878
|
} = {}) {
|
|
698
879
|
const who = person != null ? person : personName();
|
|
699
880
|
const room = folder != null ? folder : folderName(root);
|
|
@@ -718,8 +899,16 @@ function buildFirstMinute({
|
|
|
718
899
|
};
|
|
719
900
|
}
|
|
720
901
|
const tasks = loadLocalTasks(root);
|
|
721
|
-
|
|
722
|
-
|
|
902
|
+
// The rendered TODO.md still lists the init seed; in a person's room it is
|
|
903
|
+
// not their waiting work either.
|
|
904
|
+
const seedTitles = tasks.some(isHumanStarter)
|
|
905
|
+
? new Set(tasks.filter(isInitSeedTask).map((task) => String(task.title || '').trim().toLowerCase()))
|
|
906
|
+
: null;
|
|
907
|
+
const notSeed = (titles) => (titles && seedTitles
|
|
908
|
+
? titles.filter((title) => !seedTitles.has(String(title || '').trim().toLowerCase()))
|
|
909
|
+
: titles);
|
|
910
|
+
const todoBacklog = notSeed(titlesFromTodoSection(root, 'Backlog'));
|
|
911
|
+
const todoWip = notSeed(titlesFromTodoSection(root, 'In Progress'));
|
|
723
912
|
const backlogCount = todoBacklog
|
|
724
913
|
? todoBacklog.length
|
|
725
914
|
: (Array.isArray(context.backlogTasks) ? context.backlogTasks.length : 0);
|
|
@@ -749,6 +938,55 @@ function buildFirstMinute({
|
|
|
749
938
|
wipCount,
|
|
750
939
|
inboxCount,
|
|
751
940
|
});
|
|
941
|
+
// The person's own first want, claimed: if their coding agent built
|
|
942
|
+
// something since, the next step is `atris done`. If nothing is built yet,
|
|
943
|
+
// the next step is the builder, not a task-plane ready line.
|
|
944
|
+
if (picked.task && picked.task.status === 'claimed' && isHumanStarter(picked.task)) {
|
|
945
|
+
const built = listBuiltSince(root, taskStartedAt(picked.task));
|
|
946
|
+
if (built.length) {
|
|
947
|
+
return {
|
|
948
|
+
kind: 'workspace',
|
|
949
|
+
text: renderStarterNewWork({ person: who, task: picked.task, files: built, folder: room }),
|
|
950
|
+
nextCommand: 'atris done',
|
|
951
|
+
task: picked.task,
|
|
952
|
+
recap,
|
|
953
|
+
starter: true,
|
|
954
|
+
built,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const build = firstTalkBuildCommand(
|
|
958
|
+
builder !== undefined ? builder : firstTalkBuilder(),
|
|
959
|
+
picked.task.title,
|
|
960
|
+
);
|
|
961
|
+
if (build) {
|
|
962
|
+
return {
|
|
963
|
+
kind: 'workspace',
|
|
964
|
+
text: renderWorkspace({
|
|
965
|
+
person: who,
|
|
966
|
+
folder: room,
|
|
967
|
+
task: picked.task,
|
|
968
|
+
recap,
|
|
969
|
+
nextCommand: build,
|
|
970
|
+
liveWork,
|
|
971
|
+
}),
|
|
972
|
+
nextCommand: build,
|
|
973
|
+
task: picked.task,
|
|
974
|
+
recap,
|
|
975
|
+
starter: true,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const shipped = !picked.task && !picked.mission && !liveWork ? shippedHumanStarter(tasks) : null;
|
|
980
|
+
if (shipped) {
|
|
981
|
+
return {
|
|
982
|
+
kind: 'workspace',
|
|
983
|
+
text: renderStarterShipped({ person: who, task: shipped, folder: room }),
|
|
984
|
+
nextCommand: WHATS_NEXT_COMMAND,
|
|
985
|
+
task: null,
|
|
986
|
+
recap,
|
|
987
|
+
shipped,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
752
990
|
return {
|
|
753
991
|
kind: 'workspace',
|
|
754
992
|
text: renderWorkspace({
|
|
@@ -863,7 +1101,8 @@ function spokenWinReason(text) {
|
|
|
863
1101
|
|
|
864
1102
|
function isKeepWorkingMinute(minute) {
|
|
865
1103
|
const next = String((minute && minute.nextCommand) || '');
|
|
866
|
-
if (next === 'atris do') return true;
|
|
1104
|
+
if (next === 'atris do' || next === 'atris done') return true;
|
|
1105
|
+
if (minute && (minute.starter || minute.shipped)) return true;
|
|
867
1106
|
return isClaimedKeepWorkingCommand(next);
|
|
868
1107
|
}
|
|
869
1108
|
|
|
@@ -913,12 +1152,24 @@ module.exports = {
|
|
|
913
1152
|
isCertifiedReview,
|
|
914
1153
|
isFirstTalkLine,
|
|
915
1154
|
isFreshWorkspace,
|
|
1155
|
+
isWhatsNextLine,
|
|
1156
|
+
humanStarterTask,
|
|
1157
|
+
listBuiltSince,
|
|
1158
|
+
mainBuiltFile,
|
|
1159
|
+
orderBuilt,
|
|
1160
|
+
shippedHumanStarter,
|
|
1161
|
+
spokenBuiltList,
|
|
1162
|
+
taskStartedAt,
|
|
1163
|
+
loadLocalTasks,
|
|
1164
|
+
firstTalkBuildCommand,
|
|
1165
|
+
renderWhatsNextAsk,
|
|
916
1166
|
isLeftoverVerbLook,
|
|
917
1167
|
isTaskNextLook,
|
|
918
1168
|
listUserVisibleWork,
|
|
919
1169
|
personName,
|
|
920
1170
|
pickNext,
|
|
921
1171
|
renderFirstTalk,
|
|
1172
|
+
firstTalkBuilder,
|
|
922
1173
|
renderLaterRemember,
|
|
923
1174
|
spokenLaterNote,
|
|
924
1175
|
visibleWorkTitle,
|