atris 3.58.7 → 3.59.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/AGENTS.md +3 -2
- package/GETTING_STARTED.md +2 -2
- package/PERSONA.md +11 -11
- package/README.md +1 -1
- package/atris/GEMINI.md +7 -6
- package/atris/PERSONA.md +4 -4
- package/atris/features/README.md +77 -6
- package/atris/skills/README.md +44 -15
- package/atris/skills/aeo/SKILL.md +10 -10
- package/atris/skills/atris/SKILL.md +1 -1
- package/atris/skills/engines/SKILL.md +69 -50
- package/atris/skills/meta/SKILL.md +4 -4
- package/bin/atris.js +8 -6
- package/commands/aeo.js +9 -12
- package/commands/analytics.js +8 -11
- package/commands/autopilot.js +79 -8
- package/commands/avail.js +1 -1
- package/commands/brain.js +3 -1
- package/commands/business.js +13 -8
- package/commands/clean.js +17 -0
- package/commands/close.js +2 -1
- package/commands/computer.js +15 -10
- package/commands/context-sync.js +3 -3
- package/commands/doc-health.js +55 -2
- package/commands/engine.js +384 -4
- package/commands/experiments.js +13 -4
- package/commands/host.js +53 -0
- package/commands/improve.js +1 -1
- package/commands/integrations.js +2 -5
- package/commands/learn.js +1 -1
- package/commands/member.js +231 -53
- package/commands/mission.js +129 -19
- package/commands/one-lap.js +58 -3
- package/commands/pack.js +2 -2
- package/commands/probe.js +11 -5
- package/commands/pull.js +1 -1
- package/commands/push.js +1 -1
- package/commands/rainmaker.js +3 -16
- package/commands/run.js +4 -1
- package/commands/scout.js +1 -1
- package/commands/search.js +5 -3
- package/commands/spaceship.js +1 -1
- package/commands/stream.js +27 -2
- package/commands/study.js +15 -9
- package/commands/sync.js +1 -1
- package/commands/task.js +13 -2
- package/commands/unknowns.js +5 -0
- package/commands/verify.js +12 -4
- package/commands/visualize.js +20 -3
- package/commands/workflow.js +23 -7
- package/lib/codex-flight.js +28 -5
- package/lib/dispatch-scout.js +31 -22
- package/lib/engine-registry.js +1482 -13
- package/lib/feedback-triage.js +2 -0
- package/lib/fleet.js +111 -12
- package/lib/host.js +336 -0
- package/lib/improve-vitals-html.js +3 -2
- package/lib/known-commands.js +1 -1
- package/lib/lesson-contradiction.js +2 -2
- package/lib/map-refs.js +226 -0
- package/lib/member-engine.js +253 -0
- package/lib/mission-room.js +4 -4
- package/lib/noninteractive.js +15 -4
- package/lib/policy-lessons.js +1 -1
- package/lib/receipt-block.js +137 -28
- package/lib/roster-markdown.js +206 -0
- package/lib/roster-models.js +179 -0
- package/lib/runner-command.js +95 -15
- package/lib/scorecard.js +4 -4
- package/lib/self-drive.js +9 -1
- package/lib/sleep-sync.js +11 -0
- package/lib/state-detection.js +28 -9
- package/lib/task-db.js +3 -2
- package/lib/todo-fallback.js +1 -1
- package/lib/wish-audit.js +10 -6
- package/lib/wish-delegate.js +29 -5
- package/mcp/atris-mcp/index.mjs +77 -24
- package/package.json +2 -5
- package/scripts/det/det.js +2 -2
- package/scripts/det/pr-description.js +2 -2
- package/scripts/det/test.js +2 -2
- package/templates/members/host/MEMBER.md +23 -0
- package/templates/members/host/MISSION.md +11 -0
- package/templates/members/host/SOUL.md +7 -0
- package/templates/members/host/context/why.md +3 -0
- package/templates/members/host/goals.json +5 -0
- package/templates/members/host/goals.md +3 -0
- package/templates/members/host/skills/host/SKILL.md +13 -0
- package/utils/api.js +1 -1
- package/utils/auth.js +17 -7
- package/utils/backend-root.js +31 -0
- package/utils/config.js +5 -4
- package/utils/owner-identity.js +61 -0
- package/atris/policies/LESSONS.md +0 -13
- package/atris/skills/autopilot/atris-autopilot.md +0 -71
package/commands/engine.js
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
// same names ride --engine on mission run / autopilot / run.
|
|
8
8
|
//
|
|
9
9
|
// atris engine roster + current default
|
|
10
|
+
// atris engine roster show every job's workers, built-in jobs first
|
|
11
|
+
// atris engine roster confirm renew every dated worker for 30 days
|
|
12
|
+
// atris engine roster session [clear] this shell's own changes
|
|
13
|
+
// atris engine roster --available tools and models on this machine
|
|
14
|
+
// atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]
|
|
15
|
+
// atris engine assign <job> --remove <tool> | --clear [--session | --everywhere]
|
|
16
|
+
// jobs are open-ended: search, build, review, or your own ("small build")
|
|
10
17
|
// atris engine cursor make cursor the default engine here
|
|
11
18
|
// atris engine reset back to the house default (atris-fast)
|
|
12
19
|
//
|
|
@@ -27,8 +34,30 @@ const {
|
|
|
27
34
|
} = require('../lib/runner-command');
|
|
28
35
|
const { parseScopeFlag } = require('../lib/cli-scope');
|
|
29
36
|
const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
|
|
37
|
+
const { teamRosterView } = require('../lib/member-engine');
|
|
38
|
+
const { engineRunsView, availableModels } = require('../lib/roster-models');
|
|
30
39
|
const {
|
|
31
40
|
ENGINE_ROLES,
|
|
41
|
+
ENGINE_JOBS,
|
|
42
|
+
ROSTER_SOURCES,
|
|
43
|
+
ROSTER_LAYERS,
|
|
44
|
+
rosterDecided,
|
|
45
|
+
rosterWorkerWalk,
|
|
46
|
+
rosterToolLabel,
|
|
47
|
+
rosterSessionKey,
|
|
48
|
+
clearSessionRoster,
|
|
49
|
+
NO_SESSION_MESSAGE,
|
|
50
|
+
rosterJobKey,
|
|
51
|
+
rosterJobLabel,
|
|
52
|
+
rosterJobNameError,
|
|
53
|
+
rosterJobRole,
|
|
54
|
+
readRosterState,
|
|
55
|
+
customRosterJobKeys,
|
|
56
|
+
setRosterPick,
|
|
57
|
+
confirmRoster,
|
|
58
|
+
rosterMaxText,
|
|
59
|
+
rosterPinFor,
|
|
60
|
+
parseRosterUntil,
|
|
32
61
|
ENGINE_DUTIES,
|
|
33
62
|
ENGINE_HEALTH_STATUSES,
|
|
34
63
|
binInstalled,
|
|
@@ -908,6 +937,7 @@ async function runEngineSeedCommand(args, root, deps = {}) {
|
|
|
908
937
|
|
|
909
938
|
function printRoster(root, { scope = 'workspace' } = {}) {
|
|
910
939
|
reconcileStaleEngineProbeErrors(root);
|
|
940
|
+
printJobRoster(root);
|
|
911
941
|
const list = roster(root);
|
|
912
942
|
const scoped = scope === 'global'
|
|
913
943
|
? list
|
|
@@ -952,10 +982,336 @@ function registryPayload(root, { scope = 'workspace', includeHidden = false } =
|
|
|
952
982
|
scope,
|
|
953
983
|
default: current.name,
|
|
954
984
|
source: current.source,
|
|
985
|
+
roster: jobRosterView(root),
|
|
955
986
|
engines: scoped,
|
|
956
987
|
};
|
|
957
988
|
}
|
|
958
989
|
|
|
990
|
+
// A hand-edited file can hold a string or a list where a pick belongs. Only
|
|
991
|
+
// an object naming an engine counts; anything else shows as no pick.
|
|
992
|
+
function rosterPickEntry(value) {
|
|
993
|
+
return value && typeof value === 'object' && !Array.isArray(value) && value.engine ? value : null;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const WORKER_SKIP_WORDS = Object.freeze({ 'not ready': 'down', expired: 'expired', 'bad model': 'names a model it cannot run' });
|
|
997
|
+
|
|
998
|
+
// One worker of a job's team as the view shows it: what it runs, and whether
|
|
999
|
+
// it leads now, backs up, or is skipped and why.
|
|
1000
|
+
function workerRow(step, index, leadIndex) {
|
|
1001
|
+
const { worker } = step;
|
|
1002
|
+
const runs = worker.error ? null : engineRunsView(worker.engine, { model: worker.model || '', effort: worker.effort || '' });
|
|
1003
|
+
const status = index === leadIndex ? 'leads' : step.skip ? 'skipped' : 'backup';
|
|
1004
|
+
const why = step.skip === 'bad line' ? `bad line: ${worker.error}` : step.skip ? WORKER_SKIP_WORDS[step.skip] : '';
|
|
1005
|
+
return {
|
|
1006
|
+
engine: worker.engine || null,
|
|
1007
|
+
model: worker.model || null,
|
|
1008
|
+
effort: worker.effort || null,
|
|
1009
|
+
max_seconds: Number(worker.max_seconds) > 0 ? Number(worker.max_seconds) : null,
|
|
1010
|
+
until: worker.until || null,
|
|
1011
|
+
line: worker.line || null,
|
|
1012
|
+
...(worker.error ? { text: worker.text || '' } : {}),
|
|
1013
|
+
status,
|
|
1014
|
+
why,
|
|
1015
|
+
runs,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
1020
|
+
const custom = key !== role || !ENGINE_ROLES.includes(key);
|
|
1021
|
+
const layerPicks = Object.fromEntries(ROSTER_LAYERS.map((scope) => [scope, (state[scope] && state[scope].picks) || {}]));
|
|
1022
|
+
const picks = Object.fromEntries(ROSTER_LAYERS.map((scope) => [scope, rosterPickEntry(layerPicks[scope][key])]));
|
|
1023
|
+
const resolved = resolveEngineForRoleRanked(role, root, {
|
|
1024
|
+
now,
|
|
1025
|
+
sessionRosterPicks: layerPicks.session,
|
|
1026
|
+
projectRosterPicks: layerPicks.project,
|
|
1027
|
+
machineRosterPicks: layerPicks.machine,
|
|
1028
|
+
...(custom ? { job: key } : {}),
|
|
1029
|
+
});
|
|
1030
|
+
// Show the layer that decided; when none did, show the first one set. A
|
|
1031
|
+
// custom job that fell back to its kind's pick did not decide on its own.
|
|
1032
|
+
const decided = rosterDecided(resolved.source) && (!custom || resolved.job === job);
|
|
1033
|
+
const source = decided ? resolved.source : ROSTER_LAYERS.find((scope) => picks[scope]) || null;
|
|
1034
|
+
const pick = source ? picks[source] : null;
|
|
1035
|
+
const walk = pick ? rosterWorkerWalk(pick, role, registry, now) : [];
|
|
1036
|
+
const leadIndex = decided && resolved.source === source ? walk.findIndex((step) => !step.skip) : -1;
|
|
1037
|
+
const good = walk.map((step, index) => ({ step, index })).filter(({ step }) => !step.worker.error);
|
|
1038
|
+
const first = good[0] || null;
|
|
1039
|
+
const status = !pick ? 'router'
|
|
1040
|
+
: first && first.step.skip === 'expired' ? 'expired'
|
|
1041
|
+
: first && leadIndex === first.index ? 'picked'
|
|
1042
|
+
: 'not ready';
|
|
1043
|
+
const lead = leadIndex === -1 ? 'none'
|
|
1044
|
+
: first && leadIndex === first.index ? 'first'
|
|
1045
|
+
: good[1] && leadIndex === good[1].index ? 'backup'
|
|
1046
|
+
: 'later';
|
|
1047
|
+
const kind = Object.keys(ENGINE_JOBS).find((name) => ENGINE_JOBS[name] === role);
|
|
1048
|
+
// What the job really runs: its first two good workers, each with its own
|
|
1049
|
+
// model and effort, or the router's engine when no line decides.
|
|
1050
|
+
const runsOf = (entry) => (entry ? engineRunsView(entry.step.worker.engine, { model: entry.step.worker.model || '', effort: entry.step.worker.effort || '' }) : null);
|
|
1051
|
+
const runs = runsOf(first);
|
|
1052
|
+
const nowRuns = resolved.engine
|
|
1053
|
+
? engineRunsView(resolved.engine.id, { model: resolved.engine.roster_model || '', effort: resolved.engine.roster_effort || '' })
|
|
1054
|
+
: null;
|
|
1055
|
+
return {
|
|
1056
|
+
job,
|
|
1057
|
+
role,
|
|
1058
|
+
...(custom ? { key, like: kind } : {}),
|
|
1059
|
+
pick,
|
|
1060
|
+
from: source ? ROSTER_SOURCES[source] : null,
|
|
1061
|
+
file: source ? state[source].file : null,
|
|
1062
|
+
session_pick: picks.session,
|
|
1063
|
+
project_pick: picks.project,
|
|
1064
|
+
machine_pick: picks.machine,
|
|
1065
|
+
engine: resolved.engine ? resolved.engine.id : null,
|
|
1066
|
+
model: resolved.engine && resolved.engine.roster_model ? resolved.engine.roster_model : null,
|
|
1067
|
+
effort: resolved.engine && resolved.engine.roster_effort ? resolved.engine.roster_effort : null,
|
|
1068
|
+
max_seconds: pick && Number(pick.max_seconds) > 0 ? Number(pick.max_seconds) : null,
|
|
1069
|
+
runs: runs || nowRuns,
|
|
1070
|
+
backup_runs: runsOf(good[1]),
|
|
1071
|
+
now_runs: nowRuns,
|
|
1072
|
+
workers: walk.map((step, index) => workerRow(step, index, leadIndex)),
|
|
1073
|
+
lead,
|
|
1074
|
+
status,
|
|
1075
|
+
reason: resolved.reason,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// The three built-in jobs first, then the owner's own jobs from this
|
|
1080
|
+
// session, this project, and the all-projects roster.
|
|
1081
|
+
function jobRosterView(root = process.cwd(), now = new Date(), state = null) {
|
|
1082
|
+
const registry = readEngineRegistry(root);
|
|
1083
|
+
const layers = state || readRosterState(root, { now });
|
|
1084
|
+
const rows = Object.entries(ENGINE_JOBS).map(([job, role]) => jobRosterRow(job, role, root, layers, registry, now));
|
|
1085
|
+
const customKeys = [...new Set(ROSTER_LAYERS.flatMap((scope) => customRosterJobKeys(layers[scope] && layers[scope].picks)))];
|
|
1086
|
+
for (const key of customKeys) {
|
|
1087
|
+
const role = rosterJobRole(key, root, {
|
|
1088
|
+
now,
|
|
1089
|
+
sessionRosterPicks: layers.session.picks,
|
|
1090
|
+
projectRosterPicks: layers.project.picks,
|
|
1091
|
+
machineRosterPicks: layers.machine.picks,
|
|
1092
|
+
});
|
|
1093
|
+
if (!role) continue;
|
|
1094
|
+
rows.push(jobRosterRow(rosterJobLabel(key), role, root, layers, registry, now, key));
|
|
1095
|
+
}
|
|
1096
|
+
return rows;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Jobs, the team, and one warning per roster line that could not be used.
|
|
1100
|
+
function rosterReport(root = process.cwd(), now = new Date()) {
|
|
1101
|
+
const state = readRosterState(root, { now });
|
|
1102
|
+
const jobs = jobRosterView(root, now, state);
|
|
1103
|
+
const team = teamRosterView(root, { now, rosterState: state });
|
|
1104
|
+
return {
|
|
1105
|
+
jobs,
|
|
1106
|
+
team: team.rows,
|
|
1107
|
+
warnings: [...state.session.warnings, ...state.project.warnings, ...state.machine.warnings, ...team.warnings],
|
|
1108
|
+
files: {
|
|
1109
|
+
session: state.session.format === 'none' ? null : state.session.file,
|
|
1110
|
+
project: state.project.format === 'none' ? null : state.project.file,
|
|
1111
|
+
machine: state.machine.format === 'none' ? null : state.machine.file,
|
|
1112
|
+
},
|
|
1113
|
+
session: state.session.key || null,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function untilText(worker) {
|
|
1118
|
+
const until = parseRosterUntil(worker.until);
|
|
1119
|
+
if (until) return `until ${until.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }).toLowerCase()}`;
|
|
1120
|
+
return worker.never_expires ? 'no end date' : 'until no valid date';
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Every worker under its job, numbered in order, when the job has more than
|
|
1124
|
+
// the lead and one backup (the job's own line already shows those two).
|
|
1125
|
+
function renderWorkerLines(row) {
|
|
1126
|
+
if (!row.workers || row.workers.length <= 2) return [];
|
|
1127
|
+
const width = Math.max(...row.workers.map((worker) => (worker.runs ? worker.runs.text : worker.text || '').length));
|
|
1128
|
+
return row.workers.map((worker, index) => {
|
|
1129
|
+
const what = (worker.runs ? worker.runs.text : worker.text || '').padEnd(width);
|
|
1130
|
+
const extras = [worker.max_seconds ? rosterMaxText(worker.max_seconds) : '', worker.until ? `until ${worker.until}` : ''].filter(Boolean);
|
|
1131
|
+
const state = worker.status === 'leads' ? 'leads now' : worker.status === 'backup' ? 'backup' : `skipped, ${worker.why}`;
|
|
1132
|
+
return ` ${index + 1}. ${what} ${[state, ...extras].join(', ')}`.trimEnd();
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function renderJobRoster(rows) {
|
|
1137
|
+
const width = Math.max(7, ...rows.map((row) => row.job.length));
|
|
1138
|
+
const ownerWidth = Math.max(24, ...rows.filter((row) => row.pick && row.runs).map((row) => row.runs.text.length));
|
|
1139
|
+
const backupWidth = Math.max(16, ...rows.filter((row) => row.backup_runs).map((row) => row.backup_runs.text.length + 7));
|
|
1140
|
+
return rows.map((row) => {
|
|
1141
|
+
const label = row.job.padEnd(width);
|
|
1142
|
+
const nowText = row.now_runs ? row.now_runs.text : 'no ready engine';
|
|
1143
|
+
const fallsTo = `${row.like ? `falls back to ${row.like}` : 'router decides'}: ${nowText}`;
|
|
1144
|
+
if (!row.pick) return `${label} no pick, ${fallsTo}`;
|
|
1145
|
+
const owner = row.runs.text.padEnd(ownerWidth);
|
|
1146
|
+
const backup = row.backup_runs ? `backup ${row.backup_runs.text}` : 'no backup';
|
|
1147
|
+
const cap = row.max_seconds ? `${rosterMaxText(row.max_seconds)}, ` : '';
|
|
1148
|
+
const date = untilText(row.pick);
|
|
1149
|
+
const fallback = row.lead === 'backup' ? 'using backup' : row.lead === 'later' ? `using ${nowText}` : fallsTo;
|
|
1150
|
+
const status = row.status === 'expired' ? `expired, ${fallback}`
|
|
1151
|
+
: row.status === 'not ready' ? `not ready, ${fallback}`
|
|
1152
|
+
: date;
|
|
1153
|
+
const where = row.file ? `${row.from} (${row.file})` : row.from;
|
|
1154
|
+
const head = `${label} ${owner} ${backup.padEnd(backupWidth)} ${cap}${status}, ${where}`.trimEnd();
|
|
1155
|
+
return [head, ...renderWorkerLines(row)].join('\n');
|
|
1156
|
+
}).join('\n');
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function renderTeamRoster(rows) {
|
|
1160
|
+
if (!rows.length) return '';
|
|
1161
|
+
const width = Math.max(6, ...rows.map((row) => row.member.length));
|
|
1162
|
+
const jobWidth = Math.max(6, ...rows.map((row) => String(row.job || '').length));
|
|
1163
|
+
const lines = rows.map((row) => {
|
|
1164
|
+
const engine = row.engine ? engineRunsView(row.engine, { model: row.model || '', effort: row.effort || '' }).text : 'no ready engine';
|
|
1165
|
+
const how = row.source === 'file' ? `from ${row.file}` : 'automatic';
|
|
1166
|
+
return `${row.member.padEnd(width)} ${String(row.job || '').padEnd(jobWidth)} ${engine.padEnd(24)} ${how}`.trimEnd();
|
|
1167
|
+
});
|
|
1168
|
+
return ['team', ...lines].join('\n');
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function renderRosterWarnings(warnings) {
|
|
1172
|
+
return warnings.map((warning) => `warning: ${warning.file} line ${warning.line} "${warning.text}" ${warning.message}.`).join('\n');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function renderRosterReport(report) {
|
|
1176
|
+
return [renderJobRoster(report.jobs), renderTeamRoster(report.team), renderRosterWarnings(report.warnings)]
|
|
1177
|
+
.filter(Boolean)
|
|
1178
|
+
.join('\n\n');
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function printJobRoster(root, now = new Date()) {
|
|
1182
|
+
console.log(renderRosterReport(rosterReport(root, now)));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const AVAILABLE_HINT = 'see which tools and models this machine has: atris engine roster --available';
|
|
1186
|
+
|
|
1187
|
+
// Each installed tool and the models it offers, from local files only.
|
|
1188
|
+
function availableReport(root) {
|
|
1189
|
+
const engines = readEngineRegistry(root, { persist: false }).engines
|
|
1190
|
+
.filter((engine) => !HIDDEN_PROFILE_NAMES.includes(engine.id))
|
|
1191
|
+
.filter((engine) => engine.installed || (engine.health && engine.health.status === 'ready'));
|
|
1192
|
+
return availableModels(engines).map((row) => ({ ...row, tool: rosterToolLabel(row.engine) }));
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function renderAvailable(rows) {
|
|
1196
|
+
if (!rows.length) return 'no tools found on this machine. run atris engine doctor to check again';
|
|
1197
|
+
const width = Math.max(...rows.map((row) => row.tool.length));
|
|
1198
|
+
return ['on this machine', ...rows.map((row) => `${row.tool.padEnd(width)} ${row.models.join(', ') || 'its own default'} (${row.source})`)].join('\n');
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Only this session's changes, from its own file.
|
|
1202
|
+
function renderSessionRoster(report, key) {
|
|
1203
|
+
const jobs = report.jobs.filter((row) => row.from === ROSTER_SOURCES.session);
|
|
1204
|
+
const team = report.team.filter((row) => row.source === 'file' && row.file === report.files.session);
|
|
1205
|
+
if (!jobs.length && !team.length) return `this session (${key}) has no roster changes. add one with: atris engine assign <job> <tool> --session`;
|
|
1206
|
+
const head = `this session (${key}) changes${report.files.session ? `, from ${report.files.session}` : ''}`;
|
|
1207
|
+
return [head, renderJobRoster(jobs), renderTeamRoster(team)].filter(Boolean).join('\n\n');
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function runRosterSessionCommand(rest, json, root, now) {
|
|
1211
|
+
const key = rosterSessionKey();
|
|
1212
|
+
if (rest[1] === 'clear') {
|
|
1213
|
+
try {
|
|
1214
|
+
const cleared = clearSessionRoster();
|
|
1215
|
+
console.log(cleared ? 'cleared this session\'s roster changes' : 'this session has no roster changes to clear');
|
|
1216
|
+
return 0;
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
console.error(err.message);
|
|
1219
|
+
return 2;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (!key) {
|
|
1223
|
+
console.error(NO_SESSION_MESSAGE);
|
|
1224
|
+
return 2;
|
|
1225
|
+
}
|
|
1226
|
+
const report = rosterReport(root, now);
|
|
1227
|
+
if (json) {
|
|
1228
|
+
const jobs = report.jobs.filter((row) => row.from === ROSTER_SOURCES.session);
|
|
1229
|
+
const team = report.team.filter((row) => row.source === 'file' && row.file === report.files.session);
|
|
1230
|
+
console.log(JSON.stringify({ session: key, file: report.files.session, jobs, team }, null, 2));
|
|
1231
|
+
} else {
|
|
1232
|
+
console.log(renderSessionRoster(report, key));
|
|
1233
|
+
}
|
|
1234
|
+
return 0;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function runRosterCommand(args, root, now = new Date()) {
|
|
1238
|
+
const json = args.includes('--json');
|
|
1239
|
+
const available = args.includes('--available');
|
|
1240
|
+
const rest = args.filter((arg) => arg !== '--json' && arg !== '--available');
|
|
1241
|
+
const known = !rest.length
|
|
1242
|
+
|| (rest.length === 1 && ['confirm', 'session'].includes(rest[0]))
|
|
1243
|
+
|| (rest.length === 2 && rest[0] === 'session' && rest[1] === 'clear');
|
|
1244
|
+
if (!known || (available && rest.length)) {
|
|
1245
|
+
console.error('usage: atris engine roster [confirm | session [clear] | --available] [--json]');
|
|
1246
|
+
return 2;
|
|
1247
|
+
}
|
|
1248
|
+
if (available) {
|
|
1249
|
+
const rows = availableReport(root);
|
|
1250
|
+
console.log(json ? JSON.stringify({ tools: rows }, null, 2) : renderAvailable(rows));
|
|
1251
|
+
return 0;
|
|
1252
|
+
}
|
|
1253
|
+
if (rest[0] === 'session') return runRosterSessionCommand(rest, json, root, now);
|
|
1254
|
+
if (rest[0] === 'confirm') confirmRoster(root, now);
|
|
1255
|
+
const report = rosterReport(root, now);
|
|
1256
|
+
if (json) console.log(JSON.stringify(report, null, 2));
|
|
1257
|
+
else console.log(`${renderRosterReport(report)}\n\n${AVAILABLE_HINT}`);
|
|
1258
|
+
return 0;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const ASSIGN_USAGE = 'usage: atris engine assign <job> <tool> [--model <m>] [--effort low|medium|high|xhigh|max] [--max "20 min"] [--days <n>] [--add] [--backup "<tool> [model]"] [--like search|build|review] [--session | --everywhere], or atris engine assign <job> --remove <tool> | --clear. jobs live in atris/ROSTER.md (~/.atris/ROSTER.md with --everywhere, this shell only with --session) and you can edit that file any time';
|
|
1262
|
+
|
|
1263
|
+
function runAssignCommand(args, root, now = new Date()) {
|
|
1264
|
+
const job = args[0] && !String(args[0]).startsWith('--') ? args[0] : '';
|
|
1265
|
+
if (!job) {
|
|
1266
|
+
console.error('name the job first: atris engine assign <job> <tool>. jobs: search, build, review, or your own, like "small build"');
|
|
1267
|
+
return 2;
|
|
1268
|
+
}
|
|
1269
|
+
if (!rosterJobKey(job)) {
|
|
1270
|
+
console.error(rosterJobNameError(job));
|
|
1271
|
+
return 2;
|
|
1272
|
+
}
|
|
1273
|
+
const switches = ['--everywhere', '--session', '--clear', '--add'];
|
|
1274
|
+
const everywhere = args.includes('--everywhere');
|
|
1275
|
+
const session = args.includes('--session');
|
|
1276
|
+
const clear = args.includes('--clear');
|
|
1277
|
+
const add = args.includes('--add');
|
|
1278
|
+
const rest = args.slice(1).filter((arg) => !switches.includes(arg));
|
|
1279
|
+
// The tool may be several words: claude code, atris fast.
|
|
1280
|
+
const toolWords = [];
|
|
1281
|
+
while (rest.length && !rest[0].startsWith('--')) toolWords.push(rest.shift());
|
|
1282
|
+
const engine = toolWords.join(' ');
|
|
1283
|
+
const flags = {};
|
|
1284
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1285
|
+
const arg = rest[i];
|
|
1286
|
+
if (!['--model', '--backup', '--days', '--like', '--effort', '--max', '--remove'].includes(arg) || !rest[i + 1] || rest[i + 1].startsWith('--')) {
|
|
1287
|
+
console.error(`invalid assign option "${arg}"`);
|
|
1288
|
+
return 2;
|
|
1289
|
+
}
|
|
1290
|
+
flags[arg.slice(2)] = rest[++i];
|
|
1291
|
+
}
|
|
1292
|
+
const removing = flags.remove !== undefined;
|
|
1293
|
+
if (clear && (engine || Object.keys(flags).length || add)) {
|
|
1294
|
+
console.error('use --clear without an engine or other options');
|
|
1295
|
+
return 2;
|
|
1296
|
+
}
|
|
1297
|
+
if (removing && (engine || Object.keys(flags).length > 1 || add)) {
|
|
1298
|
+
console.error('use --remove <tool> without other options');
|
|
1299
|
+
return 2;
|
|
1300
|
+
}
|
|
1301
|
+
if (!clear && !removing && !engine) {
|
|
1302
|
+
console.error(ASSIGN_USAGE);
|
|
1303
|
+
return 2;
|
|
1304
|
+
}
|
|
1305
|
+
try {
|
|
1306
|
+
setRosterPick(job, engine, { ...flags, clear, add, session, everywhere, now }, root);
|
|
1307
|
+
printJobRoster(root, now);
|
|
1308
|
+
return 0;
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
console.error(err.message);
|
|
1311
|
+
return 2;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
959
1315
|
function chartEngineLabel(engine) {
|
|
960
1316
|
const id = String(engine && (engine.id || engine.name) || '').trim();
|
|
961
1317
|
const models = Array.isArray(engine && engine.models)
|
|
@@ -1104,14 +1460,22 @@ function runResolveCommand(args, root) {
|
|
|
1104
1460
|
const json = args.includes('--json');
|
|
1105
1461
|
const role = args.filter((a) => !String(a).startsWith('--'))[0] || '';
|
|
1106
1462
|
if (!role) {
|
|
1107
|
-
const message = `usage: atris engine resolve <role>; roles: ${ENGINE_ROLES.join(', ')}`;
|
|
1463
|
+
const message = `usage: atris engine resolve <role> or <job>; roles: ${ENGINE_ROLES.join(', ')}; jobs: search, build, review, or your own`;
|
|
1108
1464
|
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
1109
1465
|
else console.error(message);
|
|
1110
1466
|
return 2;
|
|
1111
1467
|
}
|
|
1112
1468
|
let picked;
|
|
1113
1469
|
try {
|
|
1114
|
-
|
|
1470
|
+
// A role (navigator, executor, validator) resolves as before; any job
|
|
1471
|
+
// name, built-in or the owner's own, resolves through its roster pick.
|
|
1472
|
+
const jobRole = ENGINE_ROLES.includes(String(role).trim().toLowerCase()) ? '' : rosterJobRole(role, root);
|
|
1473
|
+
if (!ENGINE_ROLES.includes(String(role).trim().toLowerCase()) && !jobRole) {
|
|
1474
|
+
throw new Error(`Unknown role "${role}". Known roles: ${ENGINE_ROLES.join(', ')}; jobs: search, build, review, or one set with atris engine assign`);
|
|
1475
|
+
}
|
|
1476
|
+
picked = jobRole
|
|
1477
|
+
? resolveEngineForRoleRanked(jobRole, root, { job: role })
|
|
1478
|
+
: resolveEngineForRoleRanked(role, root);
|
|
1115
1479
|
} catch (err) {
|
|
1116
1480
|
if (json) console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
|
|
1117
1481
|
else console.error(err.message);
|
|
@@ -1578,7 +1942,20 @@ function runDispatchCommand(args, root) {
|
|
|
1578
1942
|
: `engine dispatch: ${err.message}`);
|
|
1579
1943
|
return 2;
|
|
1580
1944
|
}
|
|
1581
|
-
|
|
1945
|
+
// The build line's model, effort, and time cap ride along when the roster
|
|
1946
|
+
// picks this same engine for build.
|
|
1947
|
+
const pin = rosterPinFor('executor', canonical, root);
|
|
1948
|
+
return runDispatchFlight({
|
|
1949
|
+
root,
|
|
1950
|
+
taskIds,
|
|
1951
|
+
engine: canonical,
|
|
1952
|
+
prompt: promptOverride,
|
|
1953
|
+
yolo,
|
|
1954
|
+
...(base ? { checkoutBase: base } : {}),
|
|
1955
|
+
...(pin.roster_model ? { model: pin.roster_model } : {}),
|
|
1956
|
+
...(pin.roster_effort ? { effort: pin.roster_effort } : {}),
|
|
1957
|
+
...(pin.roster_max_seconds ? { maxSeconds: pin.roster_max_seconds } : {}),
|
|
1958
|
+
}).then((flight) => {
|
|
1582
1959
|
if (json) console.log(JSON.stringify(flight, null, 2));
|
|
1583
1960
|
return flight.paused.length ? 1 : 0;
|
|
1584
1961
|
});
|
|
@@ -1657,6 +2034,9 @@ function engineCommand(args = [], deps = {}) {
|
|
|
1657
2034
|
const positional = scope.args.filter((a) => !a.startsWith('--'));
|
|
1658
2035
|
const sub = (positional[0] || '').trim();
|
|
1659
2036
|
|
|
2037
|
+
if (sub === 'roster') return runRosterCommand(scope.args.slice(scope.args.indexOf('roster') + 1), root, deps.now);
|
|
2038
|
+
if (sub === 'assign') return runAssignCommand(scope.args.slice(scope.args.indexOf('assign') + 1), root, deps.now);
|
|
2039
|
+
|
|
1660
2040
|
if (sub === 'login') {
|
|
1661
2041
|
return runEngineLoginCommand(scope.args.slice(scope.args.indexOf('login') + 1), root);
|
|
1662
2042
|
}
|
|
@@ -1705,7 +2085,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
1705
2085
|
if (sub === 'help' || args.includes('--help') || args.includes('-h')) {
|
|
1706
2086
|
console.log('\n atris engine watch [<id>|latest] [--no-follow]\n follow one live transcript or list running engine work');
|
|
1707
2087
|
console.log('\n long read-only asks: atris engine ask "..." --engine agy --timeout <seconds>\n quick asks default to 120 seconds; explicit jobs allow up to 3600\n follow live work with atris engine watch latest');
|
|
1708
|
-
console.log('\n atris engine roster + current default\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> [--json]\n choose the best ready engine for navigator|executor|validator\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
2088
|
+
console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
1709
2089
|
return 0;
|
|
1710
2090
|
}
|
|
1711
2091
|
if (isFreshWorkspace(root)) {
|
package/commands/experiments.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
|
+
const { BACKEND_ROOT_HINT, resolveBackendRoot } = require('../utils/backend-root');
|
|
4
5
|
const { runTaskOnce } = require('./autopilot');
|
|
5
6
|
const {
|
|
6
7
|
appendResultsRow,
|
|
@@ -391,7 +392,7 @@ function parseRunOptions(args) {
|
|
|
391
392
|
const options = {
|
|
392
393
|
dryRun: false,
|
|
393
394
|
verbose: false,
|
|
394
|
-
backendPath:
|
|
395
|
+
backendPath: null,
|
|
395
396
|
interventions: [],
|
|
396
397
|
};
|
|
397
398
|
|
|
@@ -527,8 +528,10 @@ function buildBenchmarkArtifact(name, packDir, options) {
|
|
|
527
528
|
'atris/features/endstate/contract.md',
|
|
528
529
|
'atris/features/endstate/artifact-schema.json',
|
|
529
530
|
path.relative(cwd, path.join(packDir, 'program.md')),
|
|
530
|
-
|
|
531
|
-
|
|
531
|
+
...(backendPath ? [
|
|
532
|
+
path.relative(cwd, path.join(backendPath, 'atris', 'MAP.md')),
|
|
533
|
+
path.relative(cwd, path.join(backendPath, 'atris', 'TODO.md')),
|
|
534
|
+
] : []),
|
|
532
535
|
],
|
|
533
536
|
contextNote: [
|
|
534
537
|
`Project Endstate track: ${track}`,
|
|
@@ -555,7 +558,7 @@ function buildBenchmarkArtifact(name, packDir, options) {
|
|
|
555
558
|
? []
|
|
556
559
|
: [
|
|
557
560
|
...collectChangedFiles(cwd, beforeCli, afterCli),
|
|
558
|
-
...collectChangedFiles(backendPath, beforeBackend, afterBackend,
|
|
561
|
+
...(backendPath ? collectChangedFiles(backendPath, beforeBackend, afterBackend, `${path.relative(cwd, backendPath)}/`) : []),
|
|
559
562
|
];
|
|
560
563
|
const wikiAfter = readTextIfExists(path.join(cwd, 'atris', 'wiki', 'STATUS.md'));
|
|
561
564
|
|
|
@@ -637,6 +640,12 @@ function experimentsRun(name, ...args) {
|
|
|
637
640
|
return;
|
|
638
641
|
}
|
|
639
642
|
|
|
643
|
+
if (!options.backendPath) options.backendPath = resolveBackendRoot();
|
|
644
|
+
if (!options.backendPath) {
|
|
645
|
+
console.error(BACKEND_ROOT_HINT);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
|
|
640
649
|
const schemaPath = path.join(process.cwd(), 'atris', 'features', 'endstate', 'artifact-schema.json');
|
|
641
650
|
const contractPath = path.join(process.cwd(), 'atris', 'features', 'endstate', 'contract.md');
|
|
642
651
|
if (!fs.existsSync(schemaPath) || !fs.existsSync(contractPath)) {
|
package/commands/host.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { hostAction } = require('../lib/host');
|
|
4
|
+
|
|
5
|
+
function parse(argv) {
|
|
6
|
+
const options = {};
|
|
7
|
+
const positionals = [];
|
|
8
|
+
const flags = new Set(['id', 'name', 'team', 'manager', 'door', 'now', 'question', 'event-id', 'from', 'text', 'patch', 'expected-revision', 'source', 'evidence', 'reason', 'activity', 'as']);
|
|
9
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
10
|
+
const item = argv[i];
|
|
11
|
+
if (item === '--json') { options.json = true; continue; }
|
|
12
|
+
if (item.startsWith('--')) {
|
|
13
|
+
const key = item.slice(2);
|
|
14
|
+
if (!flags.has(key) || !argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(`invalid --${key} value`);
|
|
15
|
+
options[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = argv[++i];
|
|
16
|
+
} else positionals.push(item);
|
|
17
|
+
}
|
|
18
|
+
return { options, positionals };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function human(command, result) {
|
|
22
|
+
if (command === 'room') return result.text;
|
|
23
|
+
if (command === 'due') return result.length ? result.map((entry) => `${entry.name} (${entry.id}) is due for a question.`).join('\n') : 'no questions due';
|
|
24
|
+
if (command === 'outbox') return result.length ? result.map((entry) => `${entry.id} ${entry.kind} to ${entry.to}: ${entry.text}`).join('\n') : 'outbox empty';
|
|
25
|
+
if (command === 'view') return [...result.cards.map((entry) => entry.card), result.own ? `Your private record:\n${JSON.stringify(result.own, null, 2)}` : ''].filter(Boolean).join('\n');
|
|
26
|
+
if (command === 'receive') return result.duplicate ? 'event already recorded' : `${result.kind} recorded for ${result.id}`;
|
|
27
|
+
return `${command} complete: ${JSON.stringify(result)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hostCommand(argv = process.argv.slice(3), root = process.cwd()) {
|
|
31
|
+
let jsonOutput = argv.includes('--json');
|
|
32
|
+
try {
|
|
33
|
+
const [command, ...rest] = argv;
|
|
34
|
+
if (!command || command === '--help' || command === '-h') {
|
|
35
|
+
const usage = 'usage: atris host join|leave|pause|resume|forget|due|ask|receive|card|link|propose|outbox|sent|room|view [options]';
|
|
36
|
+
console.log(jsonOutput ? JSON.stringify({ usage }) : usage);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const { options, positionals } = parse(rest);
|
|
40
|
+
jsonOutput = options.json;
|
|
41
|
+
const args = { ...options };
|
|
42
|
+
if (['leave', 'pause', 'resume', 'forget', 'ask', 'card', 'sent'].includes(command)) args.id = positionals[0];
|
|
43
|
+
if (['link', 'propose'].includes(command)) { args.a = positionals[0]; args.b = positionals[1]; }
|
|
44
|
+
if (command === 'receive') args.eventId = options.eventId;
|
|
45
|
+
const result = hostAction(root, command, args);
|
|
46
|
+
console.log(jsonOutput ? JSON.stringify(result) : human(command, result));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(jsonOutput ? JSON.stringify({ error: error.message }) : `host: ${error.message}`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { hostCommand };
|
package/commands/improve.js
CHANGED
|
@@ -37,7 +37,7 @@ const rsi = require('../lib/rsi-record');
|
|
|
37
37
|
* writes. The path sent to the backend is left untouched, a remote tick
|
|
38
38
|
* may target a server-side `~/...` workspace the server expands itself, but
|
|
39
39
|
* local scorecard/journal writes must never create a literal `~` directory.
|
|
40
|
-
* (Surfaced by a live plan tick whose
|
|
40
|
+
* (Surfaced by a live plan tick whose home-relative path wrote junk locally.)
|
|
41
41
|
*/
|
|
42
42
|
function expandHome(p) {
|
|
43
43
|
if (typeof p === 'string' && (p === '~' || p.startsWith('~/'))) {
|
package/commands/integrations.js
CHANGED
|
@@ -35,6 +35,7 @@ const os = require('os');
|
|
|
35
35
|
const path = require('path');
|
|
36
36
|
const readline = require('readline');
|
|
37
37
|
const { spawnSync } = require('child_process');
|
|
38
|
+
const { sleepSync } = require('../lib/sleep-sync');
|
|
38
39
|
|
|
39
40
|
const CALENDAR_CACHE_PATH = path.join(os.homedir(), '.atris', 'calendar-events-cache.json');
|
|
40
41
|
const GMAIL_CONNECT_POLL_MS = 3000;
|
|
@@ -1812,10 +1813,6 @@ function writeImessageLookupCache(cache) {
|
|
|
1812
1813
|
return cachePath;
|
|
1813
1814
|
}
|
|
1814
1815
|
|
|
1815
|
-
function sleepMs(ms) {
|
|
1816
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
1816
|
function readLatestOutgoingImessage(handle, sinceMs) {
|
|
1820
1817
|
const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
|
|
1821
1818
|
const sinceUnix = Math.max(0, Math.floor(Number(sinceMs || Date.now()) / 1000) - 5);
|
|
@@ -1873,7 +1870,7 @@ function imessageVerifyLatestOutgoing(handle, sinceMs, options = {}) {
|
|
|
1873
1870
|
&& !(latest.is_sent || latest.is_delivered || latest.is_finished)
|
|
1874
1871
|
&& Date.now() - startedAt < timeoutMs
|
|
1875
1872
|
) {
|
|
1876
|
-
|
|
1873
|
+
sleepSync(intervalMs);
|
|
1877
1874
|
latest = readLatestOutgoingImessage(handle, sinceMs);
|
|
1878
1875
|
}
|
|
1879
1876
|
return {
|
package/commands/learn.js
CHANGED
|
@@ -460,7 +460,7 @@ function harvestFromJournals(deps = {}) {
|
|
|
460
460
|
const lines = notesMatch[1].trim().split('\n').filter(l => l.startsWith('- '));
|
|
461
461
|
for (const line of lines) {
|
|
462
462
|
// Strip bullet and optional timestamp prefix
|
|
463
|
-
const insight = line.replace(/^- (\d{2}:\d{2}
|
|
463
|
+
const insight = line.replace(/^- (\d{2}:\d{2} [-\u2014] )?/, '').trim();
|
|
464
464
|
if (leftoverClaimableInsight(insight)) continue;
|
|
465
465
|
if (insight.length > 10) {
|
|
466
466
|
candidates.push({ insight, source: path.basename(logPath) });
|