atris 3.74.4 → 3.74.6
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/FOR_AGENTS.md +12 -12
- package/atris/AGENTS.md +7 -7
- package/atris/CLAUDE.md +10 -10
- package/atris/policies/atris-backend.md +1 -1
- package/atris/policies/atris-design.md +71 -71
- package/atris/policies/design-seed.md +28 -28
- package/atris/policies/writing.md +1 -1
- package/atris/skills/apps/SKILL.md +3 -3
- package/atris/skills/atris-feedback/SKILL.md +2 -2
- package/atris/skills/autopilot/SKILL.md +10 -10
- package/atris/skills/blocks/SKILL.md +16 -16
- package/atris/skills/calendar/SKILL.md +1 -1
- package/atris/skills/clawhub/atris/SKILL.md +4 -4
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +7 -7
- package/atris/skills/create-app/SKILL.md +18 -18
- package/atris/skills/create-member/SKILL.md +11 -11
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/drive/SKILL.md +25 -25
- package/atris/skills/email-agent/SKILL.md +11 -11
- package/atris/skills/endgame/SKILL.md +33 -33
- package/atris/skills/magic-inbox/SKILL.md +15 -15
- package/atris/skills/notion/SKILL.md +1 -1
- package/atris/skills/ramp/SKILL.md +7 -7
- package/atris/skills/research-search/SKILL.md +7 -7
- package/atris/skills/slides/SKILL.md +7 -7
- package/atris/skills/tidy/SKILL.md +11 -11
- package/atris/skills/wake/SKILL.md +11 -11
- package/atris/skills/writing/SKILL.md +5 -5
- package/atris/team/_template/SOUL.md +3 -3
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris/team/launcher/MEMBER.md +2 -2
- package/atris/team/mission-lead/MISSION.md +7 -7
- package/atris/team/opus-overnight/MISSION.md +16 -16
- package/atris/team/opus-overnight/STEERING.md +5 -5
- package/atris.md +9 -0
- package/bin/atris.js +16 -3
- package/commands/campaign.js +17 -2
- package/commands/card.js +19 -12
- package/commands/engine.js +92 -17
- package/commands/gm.js +30 -0
- package/commands/idea.js +263 -0
- package/commands/init.js +11 -2
- package/commands/land.js +39 -18
- package/commands/mission.js +4 -3
- package/commands/ninety-nine.js +288 -0
- package/commands/one-lap.js +46 -27
- package/commands/reel.js +12 -7
- package/commands/sync.js +10 -2
- package/commands/task.js +4 -2
- package/commands/xp.js +68 -15
- package/commands/zen.js +10 -3
- package/lib/campaign-keep.js +23 -8
- package/lib/campaign-loop.js +24 -5
- package/lib/career-xp-lock.js +61 -0
- package/lib/career-xp-root.js +61 -0
- package/lib/claude-boot-block.js +7 -4
- package/lib/engine-registry.js +139 -1
- package/lib/fleet.js +11 -2
- package/lib/gm-bet.js +734 -42
- package/lib/gm-card.js +431 -0
- package/lib/gm-phone-page.js +486 -717
- package/lib/gm-phone.js +33 -34
- package/lib/gm-season.js +14 -0
- package/lib/headless-chrome.js +39 -0
- package/lib/idea-gate.js +33 -0
- package/lib/idea-inbox.js +220 -0
- package/lib/idea-ledger.js +196 -0
- package/lib/idea-tick.js +384 -0
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +1 -0
- package/lib/ninety-nine-sync.js +156 -0
- package/lib/ninety-nine.js +247 -0
- package/lib/opening-lines.js +94 -0
- package/lib/roster-markdown.js +39 -1
- package/lib/roster-models.js +1 -1
- package/lib/roster-swap.js +353 -0
- package/lib/spawn-env.js +29 -2
- package/lib/task-db.js +8 -4
- package/lib/wish-audit.js +14 -0
- package/lib/yes-lock.js +365 -256
- package/lib/zen-apply.js +6 -3
- package/lib/zen.js +28 -1
- package/package.json +1 -1
- package/templates/business-starter/CLAUDE.md +7 -7
- package/templates/business-starter/MAP.md +1 -1
- package/templates/business-starter/TODO.md +1 -1
- package/templates/business-starter/atris.md +2 -2
- package/templates/business-starter/context/README.md +6 -6
- package/templates/business-starter/context/live-workspace.md +1 -1
- package/templates/business-starter/goals.md +1 -1
- package/templates/business-starter/instructions.md +1 -1
- package/templates/business-starter/memory.md +4 -4
- package/templates/business-starter/persona.md +3 -3
- package/templates/business-starter/policies/LESSONS.md +1 -1
- package/templates/business-starter/reports/README.md +1 -1
- package/templates/business-starter/skills/README.md +1 -1
- package/templates/business-starter/team/README.md +1 -1
- package/templates/business-starter/team/START_HERE.md +1 -1
- package/templates/business-starter/team/_template/SOUL.md +3 -3
- package/templates/business-starter/team/comms/SOUL.md +2 -2
- package/templates/business-starter/team/ops/SOUL.md +4 -4
- package/templates/business-starter/team/research/SOUL.md +3 -3
- package/templates/business-starter/wiki/index.md +1 -1
- package/templates/research-canonical/CLAUDE.md +7 -7
- package/templates/research-canonical/MAP.md +1 -1
- package/templates/research-canonical/TODO.md +1 -1
- package/templates/research-canonical/atris.md +2 -2
- package/templates/research-canonical/context/README.md +8 -8
- package/templates/research-canonical/context/live-workspace.md +1 -1
- package/templates/research-canonical/goals.md +2 -2
- package/templates/research-canonical/instructions.md +1 -1
- package/templates/research-canonical/memory.md +4 -4
- package/templates/research-canonical/persona.md +1 -1
- package/templates/research-canonical/policies/LESSONS.md +1 -1
- package/templates/research-canonical/reports/README.md +1 -1
- package/templates/research-canonical/skills/README.md +1 -1
- package/templates/research-canonical/team/README.md +1 -1
- package/templates/research-canonical/team/eval/SOUL.md +1 -1
- package/templates/research-canonical/team/experiment/SOUL.md +2 -2
- package/templates/research-canonical/team/hypothesis/SOUL.md +3 -3
- package/templates/research-canonical/team/literature/SOUL.md +2 -2
- package/templates/research-canonical/wiki/briefs/research-program.md +1 -1
- package/templates/research-canonical/wiki/index.md +1 -1
- package/utils/auth.js +1 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { randomUUID } = require('node:crypto');
|
|
6
|
+
const { argsWantHelp } = require('../lib/noninteractive');
|
|
7
|
+
const { getAtrisDir, loadCredentials } = require('../utils/auth');
|
|
8
|
+
const { stateHome } = require('../lib/state-home');
|
|
9
|
+
const { validateSync, checkpointKey, syncLines } = require('../lib/ninety-nine-sync');
|
|
10
|
+
const {
|
|
11
|
+
validLength,
|
|
12
|
+
parseSessionLog,
|
|
13
|
+
foldSessions,
|
|
14
|
+
runningSession,
|
|
15
|
+
startRecord,
|
|
16
|
+
endRecord,
|
|
17
|
+
automaticEndRecord,
|
|
18
|
+
pauseRecord,
|
|
19
|
+
resumeRecord,
|
|
20
|
+
sessionStatus,
|
|
21
|
+
sessionsToday,
|
|
22
|
+
healthToday,
|
|
23
|
+
healthSummary,
|
|
24
|
+
} = require('../lib/ninety-nine');
|
|
25
|
+
|
|
26
|
+
function usage() {
|
|
27
|
+
return [
|
|
28
|
+
'usage: atris 99 focus <25|50|90>',
|
|
29
|
+
' atris 99 meditate <10|20|30>',
|
|
30
|
+
' atris 99 stop [note words]',
|
|
31
|
+
' atris 99 pause',
|
|
32
|
+
' atris 99 resume',
|
|
33
|
+
' atris 99 status [--json]',
|
|
34
|
+
' atris 99 today [--json]',
|
|
35
|
+
' atris 99 sync [--dry-run]',
|
|
36
|
+
'',
|
|
37
|
+
'sync is off without a sync block in ~/.atris/99/config.json (ATRIS_99_CONFIG overrides it).',
|
|
38
|
+
'personal: {"sync":{"target":"personal","app_slug":"project-99"}}',
|
|
39
|
+
'workspace: {"sync":{"target":"workspace","business":"your-business","workspace":"optional-id"}}',
|
|
40
|
+
'both targets merge in both directions; workspace reads atris/99/sessions.*.jsonl and legacy sessions.jsonl.',
|
|
41
|
+
'workspace writes only sessions.<device>.jsonl; its stable device id lives in the local sync state.',
|
|
42
|
+
'personal uses your user login and defaults to app_slug project-99; its records api must be deployed first.',
|
|
43
|
+
'dry-run reads cloud records and shows missing lines without saving logs or the sync cursor.',
|
|
44
|
+
].join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readJson(file, fallback) {
|
|
48
|
+
const text = readLog(file);
|
|
49
|
+
if (!text) return fallback;
|
|
50
|
+
try { return JSON.parse(text); } catch { throw new Error('check the 99 config or sync-state json'); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function saveSyncState(file, state) {
|
|
54
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
55
|
+
try {
|
|
56
|
+
fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
57
|
+
fs.renameSync(temporary, file);
|
|
58
|
+
} finally {
|
|
59
|
+
fs.rmSync(temporary, { force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function syncNinetyNine(args, { log, logPath, configPath, statePath, request, credentials = loadCredentials }) {
|
|
64
|
+
if (args.length > 1 || (args.length === 1 && args[0] !== '--dry-run')) {
|
|
65
|
+
log('usage: atris 99 sync [--dry-run]');
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
let lock;
|
|
69
|
+
let lockPath;
|
|
70
|
+
let stateLock;
|
|
71
|
+
let stateLockPath;
|
|
72
|
+
try {
|
|
73
|
+
const dir = path.join(stateHome(), '.atris', '99');
|
|
74
|
+
const config = readJson(configPath || process.env.ATRIS_99_CONFIG || path.join(dir, 'config.json'), {});
|
|
75
|
+
if (!config?.sync) {
|
|
76
|
+
log('sync is off (local only)');
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
validateSync(config.sync);
|
|
80
|
+
const creds = await credentials();
|
|
81
|
+
if (!creds?.token) throw new Error('sync needs your signed-in user login; run atris login');
|
|
82
|
+
const dryRun = args.includes('--dry-run');
|
|
83
|
+
const file = logPath || process.env.ATRIS_99_LOG || path.join(dir, 'sessions.jsonl');
|
|
84
|
+
const stateFile = statePath || path.join(dir, 'sync-state.json');
|
|
85
|
+
if (!dryRun) {
|
|
86
|
+
for (const folder of new Set([path.dirname(file), path.dirname(stateFile)])) {
|
|
87
|
+
fs.mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
88
|
+
}
|
|
89
|
+
// Serialize cursor updates, even if callers use different local logs.
|
|
90
|
+
stateLockPath = `${stateFile}.lock`;
|
|
91
|
+
stateLock = fs.openSync(stateLockPath, 'wx', 0o600);
|
|
92
|
+
lockPath = `${file}.lock`;
|
|
93
|
+
lock = fs.openSync(lockPath, 'wx', 0o600);
|
|
94
|
+
}
|
|
95
|
+
let text = readLog(file);
|
|
96
|
+
const records = parseSessionLog(text);
|
|
97
|
+
const state = readJson(stateFile, { v: 1, targets: {} });
|
|
98
|
+
if (state?.v !== 1 || !state.targets || typeof state.targets !== 'object' || Array.isArray(state.targets)) {
|
|
99
|
+
throw new Error('check the 99 sync-state json');
|
|
100
|
+
}
|
|
101
|
+
const key = checkpointKey(config.sync, creds);
|
|
102
|
+
const checkpoint = state.targets[key] || {};
|
|
103
|
+
if ((checkpoint.known !== undefined && (!Array.isArray(checkpoint.known) || !checkpoint.known.every((item) => typeof item === 'string')))
|
|
104
|
+
|| (checkpoint.since != null && !Number.isFinite(Date.parse(checkpoint.since)))) {
|
|
105
|
+
throw new Error('check the 99 sync-state json');
|
|
106
|
+
}
|
|
107
|
+
if (config.sync.target === 'workspace' && state.device_id === undefined) {
|
|
108
|
+
state.device_id = randomUUID();
|
|
109
|
+
// Save the identity before uploading, even if the reply or later log save fails.
|
|
110
|
+
// The state lock keeps simultaneous local runs on the same device file.
|
|
111
|
+
if (!dryRun) saveSyncState(stateFile, state);
|
|
112
|
+
}
|
|
113
|
+
const result = await syncLines({ records, sync: config.sync, checkpoint, deviceId: state.device_id, credentials: creds, dryRun, request });
|
|
114
|
+
if (dryRun) {
|
|
115
|
+
log(`would send ${result.send.length} lines and pull ${result.pull.length} lines (${config.sync.target})`);
|
|
116
|
+
for (const record of result.send) log(`send: ${JSON.stringify(record)}`);
|
|
117
|
+
for (const record of result.pull) log(`pull: ${JSON.stringify(record)}`);
|
|
118
|
+
} else {
|
|
119
|
+
for (const record of result.pull) text = appendRecord(file, text, record);
|
|
120
|
+
state.targets[key] = result.checkpoint;
|
|
121
|
+
saveSyncState(stateFile, state);
|
|
122
|
+
log(`sent ${result.send.length} lines and pulled ${result.pull.length} lines (${config.sync.target})`);
|
|
123
|
+
}
|
|
124
|
+
return 0;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const message = error.code === 'EEXIST' ? 'session sync is busy; try again'
|
|
127
|
+
: error.code ? 'check the sync file paths and permissions' : error.message;
|
|
128
|
+
log(`could not sync: ${String(message).replace(/\s+/g, ' ')}`);
|
|
129
|
+
return 1;
|
|
130
|
+
} finally {
|
|
131
|
+
for (const [handle, file] of [[lock, lockPath], [stateLock, stateLockPath]]) {
|
|
132
|
+
if (handle !== undefined) {
|
|
133
|
+
fs.closeSync(handle);
|
|
134
|
+
fs.unlinkSync(file);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function readLog(logPath) {
|
|
141
|
+
try { return fs.readFileSync(logPath, 'utf8'); } catch (error) {
|
|
142
|
+
if (error.code === 'ENOENT') return '';
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function appendRecord(logPath, previousText, record) {
|
|
148
|
+
// A valid last record may lack a newline. Preserve every existing byte.
|
|
149
|
+
const separator = previousText && !previousText.endsWith('\n') ? '\n' : '';
|
|
150
|
+
const line = `${separator}${JSON.stringify(record)}\n`;
|
|
151
|
+
fs.appendFileSync(logPath, line, { mode: 0o600 });
|
|
152
|
+
return previousText + line;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ninetyNineAtris(args = process.argv.slice(3), {
|
|
156
|
+
log = console.log,
|
|
157
|
+
now = () => new Date(),
|
|
158
|
+
logPath,
|
|
159
|
+
configPath,
|
|
160
|
+
statePath,
|
|
161
|
+
request,
|
|
162
|
+
credentials,
|
|
163
|
+
} = {}) {
|
|
164
|
+
const [action, ...rest] = args;
|
|
165
|
+
// Words after stop belong to its note, including the word "help".
|
|
166
|
+
if ((action !== 'stop' && argsWantHelp(args)) || (action === 'stop' && ['--help', '-h'].includes(rest[0]))) {
|
|
167
|
+
log(usage());
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
if (action === 'sync') return syncNinetyNine(rest, { log, logPath, configPath, statePath, request, credentials });
|
|
171
|
+
const json = rest.includes('--json');
|
|
172
|
+
if (action === 'focus' || action === 'meditate') {
|
|
173
|
+
if (rest.length !== 1 || !validLength(action, Number(rest[0])) || String(Number(rest[0])) !== rest[0]) {
|
|
174
|
+
log(action === 'focus' ? 'choose focus 25, 50, or 90 minutes' : 'choose meditate 10, 20, or 30 minutes');
|
|
175
|
+
return 2;
|
|
176
|
+
}
|
|
177
|
+
} else if (action === 'status' || action === 'today') {
|
|
178
|
+
if (rest.length > 1 || (rest.length === 1 && !json)) {
|
|
179
|
+
log(`usage: atris 99 ${action} [--json]`);
|
|
180
|
+
return 2;
|
|
181
|
+
}
|
|
182
|
+
} else if (action === 'pause' || action === 'resume') {
|
|
183
|
+
if (rest.length) {
|
|
184
|
+
log(`usage: atris 99 ${action}`);
|
|
185
|
+
return 2;
|
|
186
|
+
}
|
|
187
|
+
} else if (action !== 'stop') {
|
|
188
|
+
log(usage());
|
|
189
|
+
return 2;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let lock;
|
|
193
|
+
let lockPath;
|
|
194
|
+
try {
|
|
195
|
+
const override = logPath || process.env.ATRIS_99_LOG;
|
|
196
|
+
const file = override || path.join(getAtrisDir(), '99', 'sessions.jsonl');
|
|
197
|
+
const dir = path.dirname(file);
|
|
198
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
199
|
+
// An override may live in a shared folder. Only tighten our own folder.
|
|
200
|
+
if (!override) fs.chmodSync(dir, 0o700);
|
|
201
|
+
lockPath = `${file}.lock`;
|
|
202
|
+
try { lock = fs.openSync(lockPath, 'wx', 0o600); } catch (error) {
|
|
203
|
+
if (error.code === 'EEXIST') throw new Error('session log is busy; try again');
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
let text = readLog(file);
|
|
207
|
+
const records = parseSessionLog(text);
|
|
208
|
+
let sessions = foldSessions(records);
|
|
209
|
+
const time = now();
|
|
210
|
+
let active = runningSession(sessions, time);
|
|
211
|
+
const stopTarget = action === 'stop'
|
|
212
|
+
? active || sessions.findLast((session) => automaticEndRecord(session, time)?.status === 'done')
|
|
213
|
+
: null;
|
|
214
|
+
|
|
215
|
+
for (const session of sessions) {
|
|
216
|
+
const end = automaticEndRecord(session, time);
|
|
217
|
+
if (end && session !== stopTarget) {
|
|
218
|
+
text = appendRecord(file, text, end);
|
|
219
|
+
records.push(end);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (action === 'stop') {
|
|
224
|
+
if (!stopTarget) {
|
|
225
|
+
log('nothing running');
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
const end = endRecord(stopTarget, time, rest.join(' '));
|
|
229
|
+
appendRecord(file, text, end);
|
|
230
|
+
log(`${stopTarget.kind} ${end.status}: ${end.status === 'done' ? stopTarget.planned_min : 0} finished minutes`);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
sessions = foldSessions(records);
|
|
235
|
+
active = runningSession(sessions, time);
|
|
236
|
+
if (action === 'focus' || action === 'meditate') {
|
|
237
|
+
if (active) {
|
|
238
|
+
log('a session is already running; use atris 99 stop first');
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
appendRecord(file, text, startRecord(action, Number(rest[0]), time, randomUUID()));
|
|
242
|
+
log(`${action} started for ${rest[0]} minutes`);
|
|
243
|
+
} else if (action === 'pause' || action === 'resume') {
|
|
244
|
+
let record;
|
|
245
|
+
try {
|
|
246
|
+
record = action === 'pause' ? pauseRecord(active, time) : resumeRecord(active, time);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
log(error.message);
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
appendRecord(file, text, record);
|
|
252
|
+
log(`${active.kind} ${action === 'pause' ? 'paused' : 'resumed'}`);
|
|
253
|
+
} else if (action === 'status') {
|
|
254
|
+
const result = sessionStatus(active, time);
|
|
255
|
+
if (json) log(JSON.stringify(result));
|
|
256
|
+
else if (!active) log('nothing running');
|
|
257
|
+
else {
|
|
258
|
+
const seconds = result.running.remaining_seconds;
|
|
259
|
+
log(`${active.kind}: ${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')} left${active.status === 'paused' ? ' (paused)' : ''}`);
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
const result = sessionsToday(sessions, time);
|
|
263
|
+
const health = healthToday(records, time);
|
|
264
|
+
if (health) result.health_day = health;
|
|
265
|
+
if (json) log(JSON.stringify(result));
|
|
266
|
+
else {
|
|
267
|
+
for (const session of result.sessions) {
|
|
268
|
+
log(`${session.kind} ${session.planned_min} minutes: ${session.status}`);
|
|
269
|
+
}
|
|
270
|
+
log(`${result.finished_minutes} finished minutes today`);
|
|
271
|
+
log(`Today: ${result.counts.focus} focus, ${result.counts.meditate} meditate, ${result.finished_minutes} min`);
|
|
272
|
+
if (health) log(healthSummary(health));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return 0;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
const message = error.code ? 'check the file path and its permissions' : error.message;
|
|
278
|
+
log(`could not use the session log: ${String(message).replace(/\s+/g, ' ')}`);
|
|
279
|
+
return 1;
|
|
280
|
+
} finally {
|
|
281
|
+
if (lock !== undefined) {
|
|
282
|
+
fs.closeSync(lock);
|
|
283
|
+
fs.unlinkSync(lockPath);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = { ninetyNineAtris };
|
package/commands/one-lap.js
CHANGED
|
@@ -242,6 +242,18 @@ function lapModelPins(executor, validators = []) {
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
// The roster pins, then a caller's overrides: a time cap for the build wins
|
|
246
|
+
// over the roster's, and a caller's house rules ride at the end of the
|
|
247
|
+
// builder's brief (atris idea tick sends both).
|
|
248
|
+
function lapFlightPins(executor, validators = [], options = {}) {
|
|
249
|
+
const pins = lapModelPins(executor, validators);
|
|
250
|
+
const cap = Math.floor(Number(options.maxSeconds));
|
|
251
|
+
if (Number.isFinite(cap) && cap > 0) pins.maxSeconds = cap;
|
|
252
|
+
const brief = String(options.brief || '').trim();
|
|
253
|
+
if (brief) pins.brief = brief;
|
|
254
|
+
return pins;
|
|
255
|
+
}
|
|
256
|
+
|
|
245
257
|
function taskById(runCli, taskId) {
|
|
246
258
|
const result = runCli(['task', 'show', taskId, '--json']);
|
|
247
259
|
if (!result || result.status !== 0) return null;
|
|
@@ -426,10 +438,16 @@ async function runOneLap(ask, options = {}) {
|
|
|
426
438
|
const asJson = options.asJson === true;
|
|
427
439
|
const runCli = options.ownCli || ((args, cwd = root) => ownCli(root, args, cwd));
|
|
428
440
|
const progress = options.progress || ((line) => process.stderr.write(`${line}\n`));
|
|
441
|
+
// A caller that drives laps itself (atris idea tick) reads the result object
|
|
442
|
+
// instead of parsing printed text, and can keep the lap quiet.
|
|
443
|
+
const report = (result) => {
|
|
444
|
+
if (typeof options.onResult === 'function') options.onResult(result);
|
|
445
|
+
if (options.quiet !== true) emit(result, asJson);
|
|
446
|
+
};
|
|
429
447
|
const text = String(ask || '').replace(/\s+/g, ' ').trim();
|
|
430
448
|
if (!text) {
|
|
431
449
|
const result = { schema: 'atris.one_lap.v1', ok: false, status: 'stuck', reason: 'a request is required', next_action: 'atris "<request>"' };
|
|
432
|
-
|
|
450
|
+
report(result);
|
|
433
451
|
return 2;
|
|
434
452
|
}
|
|
435
453
|
|
|
@@ -453,7 +471,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
453
471
|
: oneLapRetryCommand(text, options),
|
|
454
472
|
});
|
|
455
473
|
result.resumed = true;
|
|
456
|
-
|
|
474
|
+
report(result);
|
|
457
475
|
return 0;
|
|
458
476
|
}
|
|
459
477
|
|
|
@@ -467,7 +485,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
467
485
|
nextAction: wishAnswerCommand(existing.wish),
|
|
468
486
|
});
|
|
469
487
|
result.resumed = true;
|
|
470
|
-
|
|
488
|
+
report(result);
|
|
471
489
|
return 0;
|
|
472
490
|
}
|
|
473
491
|
|
|
@@ -483,7 +501,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
483
501
|
reason: 'the lap verifier is frozen after intake and cannot be replaced on retry',
|
|
484
502
|
next_action: oneLapRetryCommand(text, { engine: options.engine || '', verifier: existingVerifier }),
|
|
485
503
|
};
|
|
486
|
-
|
|
504
|
+
report(result);
|
|
487
505
|
return 2;
|
|
488
506
|
}
|
|
489
507
|
if (task && ['review', 'done'].includes(String(task.status || ''))) {
|
|
@@ -500,7 +518,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
500
518
|
reason: 'the matching task is in Review without a passing matching one-lap receipt',
|
|
501
519
|
next_action: `atris task show ${shellQuote(task.display_id)}`,
|
|
502
520
|
};
|
|
503
|
-
|
|
521
|
+
report(result);
|
|
504
522
|
return 1;
|
|
505
523
|
}
|
|
506
524
|
const result = {
|
|
@@ -528,7 +546,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
528
546
|
master_changed: false,
|
|
529
547
|
...(prior && prior.receipt && prior.receipt.result ? { result: prior.receipt.result } : {}),
|
|
530
548
|
};
|
|
531
|
-
|
|
549
|
+
report(result);
|
|
532
550
|
return 0;
|
|
533
551
|
}
|
|
534
552
|
if (task && String(task.status || '') === 'claimed') {
|
|
@@ -543,7 +561,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
543
561
|
nextAction: `atris task show ${shellQuote(task.display_id)}`,
|
|
544
562
|
});
|
|
545
563
|
result.resumed = true;
|
|
546
|
-
|
|
564
|
+
report(result);
|
|
547
565
|
return 0;
|
|
548
566
|
}
|
|
549
567
|
const released = runCli(['task', 'release', task.display_id, '--as', actor, '--json']);
|
|
@@ -555,7 +573,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
555
573
|
reason: `the abandoned one-lap claim could not be released: ${detail}`,
|
|
556
574
|
next_action: `atris task show ${shellQuote(task.display_id)}`,
|
|
557
575
|
};
|
|
558
|
-
|
|
576
|
+
report(result);
|
|
559
577
|
return 1;
|
|
560
578
|
}
|
|
561
579
|
task = taskById(runCli, task.display_id);
|
|
@@ -568,7 +586,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
568
586
|
// still has a front door: `atris wish` runs it through the gated flow.
|
|
569
587
|
// Hand the new person that exact command, not unactionable prose.
|
|
570
588
|
const result = { ...resultBase({ status: 'stuck', ask: text }), reason: intakeSafetyIssue, next_action: `atris wish ${shellQuote(text)}` };
|
|
571
|
-
|
|
589
|
+
report(result);
|
|
572
590
|
return 2;
|
|
573
591
|
}
|
|
574
592
|
}
|
|
@@ -578,12 +596,12 @@ async function runOneLap(ask, options = {}) {
|
|
|
578
596
|
executor = readyExecutor(root, options.engine || '');
|
|
579
597
|
} catch (error) {
|
|
580
598
|
const result = { ...resultBase({ status: 'stuck', ask: text }), reason: error.message, next_action: 'atris engine' };
|
|
581
|
-
|
|
599
|
+
report(result);
|
|
582
600
|
return 2;
|
|
583
601
|
}
|
|
584
602
|
if (!executor) {
|
|
585
603
|
const result = { ...resultBase({ status: 'stuck', ask: text }), reason: 'no ready executor engine is installed', next_action: 'atris engine' };
|
|
586
|
-
|
|
604
|
+
report(result);
|
|
587
605
|
return 2;
|
|
588
606
|
}
|
|
589
607
|
|
|
@@ -593,7 +611,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
593
611
|
const parsedIntakeVerifier = parseVerifyCommand(intakeVerifier);
|
|
594
612
|
if (!parsedIntakeVerifier.ok) {
|
|
595
613
|
const result = { ...resultBase({ status: 'stuck', ask: text, engine: executor, verifier: intakeVerifier }), reason: `the verifier is not safe to run (${parsedIntakeVerifier.reason})`, next_action: 'choose a local test command' };
|
|
596
|
-
|
|
614
|
+
report(result);
|
|
597
615
|
return 2;
|
|
598
616
|
}
|
|
599
617
|
}
|
|
@@ -609,7 +627,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
609
627
|
if (!payload) {
|
|
610
628
|
const detail = String(captured && (captured.stderr || captured.stdout) || 'wish intake failed').trim().slice(-500);
|
|
611
629
|
const result = { ...resultBase({ status: 'stuck', ask: text, engine: executor }), reason: detail, next_action: oneLapRetryCommand(text, { engine: executor.id, verifier: intakeVerifier }) };
|
|
612
|
-
|
|
630
|
+
report(result);
|
|
613
631
|
return captured && captured.status === 2 ? 2 : 1;
|
|
614
632
|
}
|
|
615
633
|
const durableWish = readWishes(root).find((row) => row.id === payload.wish_id);
|
|
@@ -621,12 +639,12 @@ async function runOneLap(ask, options = {}) {
|
|
|
621
639
|
question: Array.isArray(payload.questions) ? payload.questions[0] : '',
|
|
622
640
|
nextAction: wishAnswerCommand(wish),
|
|
623
641
|
});
|
|
624
|
-
|
|
642
|
+
report(result);
|
|
625
643
|
return 0;
|
|
626
644
|
}
|
|
627
645
|
if (payload.status !== 'delegated' || !payload.task_id || !payload.mission_id) {
|
|
628
646
|
const result = { ...resultBase({ status: 'stuck', ask: text, wish, engine: executor }), reason: 'wish intake did not produce one scoped task and mission', next_action: `atris wish show ${payload.wish_id}` };
|
|
629
|
-
|
|
647
|
+
report(result);
|
|
630
648
|
return 2;
|
|
631
649
|
}
|
|
632
650
|
mission = listMissions(root).find((row) => row.id === payload.mission_id) || null;
|
|
@@ -635,13 +653,13 @@ async function runOneLap(ask, options = {}) {
|
|
|
635
653
|
|
|
636
654
|
if (!mission || !task) {
|
|
637
655
|
const result = { ...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor }), reason: 'the scoped task or mission could not be loaded', next_action: wish && wish.id ? `atris wish show ${wish.id}` : 'atris wish list' };
|
|
638
|
-
|
|
656
|
+
report(result);
|
|
639
657
|
return 1;
|
|
640
658
|
}
|
|
641
659
|
const safetyIssue = oneLapSafetyIssue(task);
|
|
642
660
|
if (safetyIssue) {
|
|
643
661
|
const result = { ...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor }), reason: safetyIssue, next_action: `atris task show ${task.display_id}` };
|
|
644
|
-
|
|
662
|
+
report(result);
|
|
645
663
|
return 2;
|
|
646
664
|
}
|
|
647
665
|
let frozenVerifier = missionVerifier(mission);
|
|
@@ -652,7 +670,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
652
670
|
reason: 'the lap verifier is frozen after intake and cannot be replaced on retry',
|
|
653
671
|
next_action: oneLapRetryCommand(text, { engine: options.engine || '', verifier: frozenVerifier }),
|
|
654
672
|
};
|
|
655
|
-
|
|
673
|
+
report(result);
|
|
656
674
|
return 2;
|
|
657
675
|
}
|
|
658
676
|
const verifier = frozenVerifier || requestedVerifier || projectVerifier(root);
|
|
@@ -666,13 +684,13 @@ async function runOneLap(ask, options = {}) {
|
|
|
666
684
|
nextAction: oneLapRetryCommand(text, { engine: options.engine || '', verifier: '<command>' }),
|
|
667
685
|
});
|
|
668
686
|
result.engine = executor.id;
|
|
669
|
-
|
|
687
|
+
report(result);
|
|
670
688
|
return 0;
|
|
671
689
|
}
|
|
672
690
|
const parsedVerifier = parseVerifyCommand(verifier);
|
|
673
691
|
if (!parsedVerifier.ok) {
|
|
674
692
|
const result = { ...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor, verifier }), reason: `the mission has no safe runnable verifier (${parsedVerifier.reason})`, next_action: `atris mission status ${mission.id}` };
|
|
675
|
-
|
|
693
|
+
report(result);
|
|
676
694
|
return 2;
|
|
677
695
|
}
|
|
678
696
|
if (!frozenVerifier) {
|
|
@@ -685,7 +703,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
685
703
|
reason: `the verifier could not be frozen before dispatch: ${error.message}`,
|
|
686
704
|
next_action: `atris mission status ${mission.id}`,
|
|
687
705
|
};
|
|
688
|
-
|
|
706
|
+
report(result);
|
|
689
707
|
return 1;
|
|
690
708
|
}
|
|
691
709
|
}
|
|
@@ -697,7 +715,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
697
715
|
reason: 'no distinct ready validator engine is installed',
|
|
698
716
|
next_action: 'atris engine',
|
|
699
717
|
};
|
|
700
|
-
|
|
718
|
+
report(result);
|
|
701
719
|
return 2;
|
|
702
720
|
}
|
|
703
721
|
|
|
@@ -711,7 +729,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
711
729
|
reviewOnly: true,
|
|
712
730
|
verifierCommand: verifier,
|
|
713
731
|
validatorEngines: validators.map((entry) => entry.id),
|
|
714
|
-
...
|
|
732
|
+
...lapFlightPins(executor, validators, options),
|
|
715
733
|
actor: mission.owner || 'mission-lead',
|
|
716
734
|
installedEngines: engineRegistryView(root)
|
|
717
735
|
.filter((entry) => engineReadyAt(entry))
|
|
@@ -738,7 +756,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
738
756
|
});
|
|
739
757
|
} catch (error) {
|
|
740
758
|
const result = { ...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor, verifier }), reason: error.message || String(error), next_action: `atris task show ${task.display_id}` };
|
|
741
|
-
|
|
759
|
+
report(result);
|
|
742
760
|
return 1;
|
|
743
761
|
}
|
|
744
762
|
|
|
@@ -769,7 +787,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
769
787
|
worktree: paused.worktree || null,
|
|
770
788
|
next_action: paused.worktree ? `cd ${shellQuote(paused.worktree)} && atris worktree guard` : `atris task show ${shellQuote(task.display_id)}`,
|
|
771
789
|
};
|
|
772
|
-
|
|
790
|
+
report(result);
|
|
773
791
|
return 1;
|
|
774
792
|
}
|
|
775
793
|
|
|
@@ -790,7 +808,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
790
808
|
worktree: ready.worktree,
|
|
791
809
|
next_action: `atris mission status ${mission.id}`,
|
|
792
810
|
};
|
|
793
|
-
|
|
811
|
+
report(result);
|
|
794
812
|
return 1;
|
|
795
813
|
}
|
|
796
814
|
|
|
@@ -819,7 +837,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
819
837
|
master_changed: false,
|
|
820
838
|
result: flight.result,
|
|
821
839
|
};
|
|
822
|
-
|
|
840
|
+
report(result);
|
|
823
841
|
return 0;
|
|
824
842
|
} finally {
|
|
825
843
|
releaseAskLock(lapLock);
|
|
@@ -832,4 +850,5 @@ module.exports = {
|
|
|
832
850
|
readyExecutor,
|
|
833
851
|
readyValidators,
|
|
834
852
|
lapModelPins,
|
|
853
|
+
lapFlightPins,
|
|
835
854
|
};
|
package/commands/reel.js
CHANGED
|
@@ -14,6 +14,7 @@ const { promisify } = require('util');
|
|
|
14
14
|
const pexec = promisify(execFile);
|
|
15
15
|
const { buildReelFrame, reelFrames } = require('../lib/reel');
|
|
16
16
|
const { SIZES, KINDS } = require('../lib/card');
|
|
17
|
+
const { makeProfileDir, removeProfileDir, headlessArgs } = require('../lib/headless-chrome');
|
|
17
18
|
|
|
18
19
|
function parseFlags(argv) {
|
|
19
20
|
const flags = {}; const pos = [];
|
|
@@ -56,12 +57,16 @@ async function renderFrames(chrome, ts, spec, dir, w, h, onDone) {
|
|
|
56
57
|
const hf = path.join(dir, `f-${pad(i)}.html`);
|
|
57
58
|
const pf = path.join(dir, `f-${pad(i)}.png`);
|
|
58
59
|
fs.writeFileSync(hf, html);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
const profile = makeProfileDir('atris-reel-chrome-');
|
|
61
|
+
try {
|
|
62
|
+
await pexec(chrome, headlessArgs(profile, [
|
|
63
|
+
'--disable-gpu', '--hide-scrollbars', '--force-device-scale-factor=1',
|
|
64
|
+
`--window-size=${w},${h}`, '--virtual-time-budget=2200',
|
|
65
|
+
`--screenshot=${pf}`, `file://${hf}`,
|
|
66
|
+
]), { timeout: 60000 });
|
|
67
|
+
} finally {
|
|
68
|
+
removeProfileDir(profile);
|
|
69
|
+
}
|
|
65
70
|
onDone && onDone(++done, ts.length);
|
|
66
71
|
}
|
|
67
72
|
}
|
|
@@ -125,4 +130,4 @@ async function run(argv) {
|
|
|
125
130
|
return 0;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
module.exports = { run, findChrome, findFfmpeg };
|
|
133
|
+
module.exports = { run, findChrome, findFfmpeg, renderFrames };
|
package/commands/sync.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { stateHome } = require('../lib/state-home');
|
|
4
4
|
const { ensureWikiScaffold } = require('../lib/wiki');
|
|
5
5
|
const { upsertAtrisClaudeBootBlock } = require('../lib/claude-boot-block');
|
|
6
|
+
const { openingLinesForRoot, upsertAgentsOpeningLines } = require('../lib/opening-lines');
|
|
6
7
|
const { ensureRunIgnore } = require('../lib/gitignore');
|
|
7
8
|
const {
|
|
8
9
|
upsertAgentVoiceCard,
|
|
@@ -647,6 +648,13 @@ function syncAtris(options = {}) {
|
|
|
647
648
|
updated++;
|
|
648
649
|
}
|
|
649
650
|
|
|
651
|
+
const openingLines = openingLinesForRoot(process.cwd());
|
|
652
|
+
const agentsOpeningResult = upsertAgentsOpeningLines(agentsMdFile, openingLines, { dryRun });
|
|
653
|
+
if (agentsOpeningResult.action !== 'unchanged') {
|
|
654
|
+
console.log(`${dryRun ? 'Would update' : '✓ Updated'} AGENTS.md operating rules`);
|
|
655
|
+
updated++;
|
|
656
|
+
}
|
|
657
|
+
|
|
650
658
|
// Update .claude/skills/atris/SKILL.md (legacy - now handled above, keeping for compatibility)
|
|
651
659
|
const claudeSkillsDir = path.join(process.cwd(), '.claude', 'skills', 'atris');
|
|
652
660
|
const claudeSkillFile = path.join(claudeSkillsDir, 'SKILL.md');
|
|
@@ -727,7 +735,7 @@ Key behaviors:
|
|
|
727
735
|
|
|
728
736
|
if (fs.existsSync(rootClaudeMd)) {
|
|
729
737
|
const source = fs.readFileSync(rootClaudeMd, 'utf8');
|
|
730
|
-
const result = upsertAtrisClaudeBootBlock(source);
|
|
738
|
+
const result = upsertAtrisClaudeBootBlock(source, { openingLines });
|
|
731
739
|
if (result.action === 'repaired') {
|
|
732
740
|
applyClaudeUpdate(result.content, 'repair atris block in CLAUDE.md (missing end marker)', 'repaired atris block in CLAUDE.md (missing end marker)');
|
|
733
741
|
} else if (result.action === 'updated') {
|
|
@@ -737,7 +745,7 @@ Key behaviors:
|
|
|
737
745
|
}
|
|
738
746
|
} else {
|
|
739
747
|
// Create new CLAUDE.md with just Atris block
|
|
740
|
-
const content = upsertAtrisClaudeBootBlock().content;
|
|
748
|
+
const content = upsertAtrisClaudeBootBlock('', { openingLines }).content;
|
|
741
749
|
applyClaudeUpdate(content, 'create CLAUDE.md with atris block', 'created CLAUDE.md with atris block');
|
|
742
750
|
}
|
|
743
751
|
|
package/commands/task.js
CHANGED
|
@@ -393,7 +393,8 @@ function refreshCareerXpProjection(workspaceRoot) {
|
|
|
393
393
|
if (!workspaceRoot) return null;
|
|
394
394
|
try {
|
|
395
395
|
const { collectLocalXpProjection } = require('../commands/xp');
|
|
396
|
-
|
|
396
|
+
const { careerXpRoot } = require('../lib/career-xp-root');
|
|
397
|
+
return collectLocalXpProjection(['--workspace', careerXpRoot(workspaceRoot)]);
|
|
397
398
|
} catch (error) {
|
|
398
399
|
return {
|
|
399
400
|
ok: false,
|
|
@@ -5486,7 +5487,7 @@ function cmdAcceptGroup(args) {
|
|
|
5486
5487
|
proof,
|
|
5487
5488
|
});
|
|
5488
5489
|
if (!done.updated) { accepted.push({ id: task.id, ok: false, reason: 'not_review' }); continue; }
|
|
5489
|
-
taskDb.reviewTask(db, {
|
|
5490
|
+
const reviewed = taskDb.reviewTask(db, {
|
|
5490
5491
|
id: task.id,
|
|
5491
5492
|
actor,
|
|
5492
5493
|
reward: 1,
|
|
@@ -5495,6 +5496,7 @@ function cmdAcceptGroup(args) {
|
|
|
5495
5496
|
proof,
|
|
5496
5497
|
careerXpEligible: isVerified, // honest: career XP ONLY for rows the human actually verified
|
|
5497
5498
|
});
|
|
5499
|
+
refreshCareerXpAfterReview(reviewed);
|
|
5498
5500
|
stampAcceptGroupMetadata(taskDb, db, task.id, { actor, group: groupLabel, verified: isVerified, sampleVerifiedIds: [...verifiedIds] });
|
|
5499
5501
|
accepted.push({ id: task.id, ok: true, verified: isVerified, career_xp: isVerified });
|
|
5500
5502
|
}
|