atris 3.74.3 → 3.74.5
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/lib/campaign-keep.js +23 -8
- package/lib/campaign-loop.js +24 -5
- 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 +338 -34
- package/lib/gm-phone-page.js +325 -33
- package/lib/gm-phone.js +5 -1
- 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/wish-audit.js +14 -0
- package/lib/yes-lock.js +358 -241
- package/lib/zen-apply.js +5 -3
- package/lib/zen.js +13 -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,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const LENGTHS = { focus: [25, 50, 90], meditate: [10, 20, 30] };
|
|
4
|
+
const SOURCES = ['cli', 'mac', 'ios'];
|
|
5
|
+
const PAUSE_LIMIT_MS = 30 * 60000;
|
|
6
|
+
const HEALTH_FIELDS = ['sleep_min', 'steps', 'workout_min', 'mindful_min'];
|
|
7
|
+
|
|
8
|
+
function lineIdentity(record) {
|
|
9
|
+
return `${record.type}|${record.type === 'health_day' ? record.date : record.id}|${record.at}|${record.source}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function uniqueLines(records) {
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
return records.filter((record) => {
|
|
15
|
+
const key = lineIdentity(record);
|
|
16
|
+
if (seen.has(key)) return false;
|
|
17
|
+
seen.add(key);
|
|
18
|
+
return true;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validLength(kind, minutes) {
|
|
23
|
+
return Object.hasOwn(LENGTHS, kind) && LENGTHS[kind].includes(minutes);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseSessionLog(text) {
|
|
27
|
+
return text.split('\n').flatMap((line, index) => {
|
|
28
|
+
if (!line.trim()) return [];
|
|
29
|
+
let record;
|
|
30
|
+
try { record = JSON.parse(line); } catch { /* Report the line below. */ }
|
|
31
|
+
const common = record && record.v === 1 && SOURCES.includes(record.source)
|
|
32
|
+
&& typeof record.at === 'string' && Number.isFinite(Date.parse(record.at));
|
|
33
|
+
const session = typeof record?.id === 'string' && record.id;
|
|
34
|
+
const start = record?.type === 'start' && validLength(record.kind, record.planned_min);
|
|
35
|
+
const end = record?.type === 'end' && ['done', 'partial'].includes(record.status)
|
|
36
|
+
&& (record.inferred === undefined || typeof record.inferred === 'boolean')
|
|
37
|
+
&& (record.note === undefined || (typeof record.note === 'string' && !/[\r\n\u2028\u2029]/.test(record.note)));
|
|
38
|
+
const pause = ['pause', 'resume'].includes(record?.type);
|
|
39
|
+
const health = record?.type === 'health_day' && typeof record.date === 'string'
|
|
40
|
+
&& /^\d{4}-\d{2}-\d{2}$/.test(record.date)
|
|
41
|
+
&& Number.isFinite(Date.parse(record.date))
|
|
42
|
+
&& new Date(record.date).toISOString().slice(0, 10) === record.date
|
|
43
|
+
&& HEALTH_FIELDS.every((field) => record[field] === undefined
|
|
44
|
+
|| (Number.isFinite(record[field]) && record[field] >= 0));
|
|
45
|
+
if (!common || (!health && !(session && (start || end || pause)))) {
|
|
46
|
+
throw new Error(`session log has an invalid record on line ${index + 1}; nothing was written`);
|
|
47
|
+
}
|
|
48
|
+
return [record];
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// records -> sessions -> overdue ends -> the requested action
|
|
53
|
+
function foldSessions(records) {
|
|
54
|
+
const sessions = new Map();
|
|
55
|
+
const events = new Map();
|
|
56
|
+
const ends = new Map();
|
|
57
|
+
// Stable sort preserves append order when timestamps tie, without rewriting the log.
|
|
58
|
+
for (const record of uniqueLines(records).sort((a, b) => Date.parse(a.at) - Date.parse(b.at))) {
|
|
59
|
+
if (record.type === 'health_day') continue;
|
|
60
|
+
if (record.type === 'start') {
|
|
61
|
+
if (sessions.has(record.id)) throw new Error('session log repeats a start; nothing was written');
|
|
62
|
+
sessions.set(record.id, {
|
|
63
|
+
id: record.id,
|
|
64
|
+
kind: record.kind,
|
|
65
|
+
planned_min: record.planned_min,
|
|
66
|
+
started_at: record.at,
|
|
67
|
+
planned_end_at: new Date(Date.parse(record.at) + record.planned_min * 60000).toISOString(),
|
|
68
|
+
ended_at: null,
|
|
69
|
+
status: 'running',
|
|
70
|
+
});
|
|
71
|
+
events.set(record.id, []);
|
|
72
|
+
} else {
|
|
73
|
+
const session = sessions.get(record.id);
|
|
74
|
+
if (!session) throw new Error(`session log has an ${record.type} without a start; nothing was written`);
|
|
75
|
+
if (record.type !== 'end') {
|
|
76
|
+
events.get(record.id).push(record);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const previous = ends.get(record.id);
|
|
80
|
+
// A person's end wins over an automatic observation, in either log order.
|
|
81
|
+
if (!previous || (previous.inferred === true && record.inferred !== true)
|
|
82
|
+
|| ((previous.inferred === true) === (record.inferred === true)
|
|
83
|
+
&& Date.parse(record.at) < Date.parse(previous.at))) {
|
|
84
|
+
ends.set(record.id, record);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const session of sessions.values()) {
|
|
89
|
+
const end = ends.get(session.id);
|
|
90
|
+
const timeline = events.get(session.id).sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
91
|
+
for (const event of timeline) {
|
|
92
|
+
const at = Date.parse(event.at);
|
|
93
|
+
if (end && at > Date.parse(end.at)) break;
|
|
94
|
+
if (session.status === 'running') {
|
|
95
|
+
// A late pause cannot interrupt an already completed active stretch.
|
|
96
|
+
if (at >= Date.parse(session.planned_end_at)) break;
|
|
97
|
+
if (event.type !== 'pause') continue;
|
|
98
|
+
session.status = 'paused';
|
|
99
|
+
session.paused_at = event.at;
|
|
100
|
+
session.paused_ms = session.paused_ms || 0;
|
|
101
|
+
} else if (event.type === 'resume') {
|
|
102
|
+
const duration = at - Date.parse(session.paused_at);
|
|
103
|
+
if (duration > PAUSE_LIMIT_MS) {
|
|
104
|
+
session.pause_expired_at = session.paused_at;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
session.paused_ms += duration;
|
|
108
|
+
session.planned_end_at = new Date(Date.parse(session.planned_end_at) + duration).toISOString();
|
|
109
|
+
session.paused_at = null;
|
|
110
|
+
session.status = 'running';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (end) {
|
|
114
|
+
session.ended_at = end.at;
|
|
115
|
+
session.status = end.status;
|
|
116
|
+
if (end.note !== undefined) session.note = end.note;
|
|
117
|
+
if (end.inferred === true) session.inferred = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...sessions.values()];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function runningSession(sessions, now) {
|
|
124
|
+
const running = sessions.filter((session) => ['running', 'paused'].includes(session.status)
|
|
125
|
+
&& !automaticEndRecord(session, now));
|
|
126
|
+
if (running.length > 1) throw new Error('session log has more than one running session; nothing was written');
|
|
127
|
+
return running[0] || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function startRecord(kind, plannedMin, now, id) {
|
|
131
|
+
if (!validLength(kind, plannedMin)) throw new Error('choose a supported session length');
|
|
132
|
+
return { v: 1, type: 'start', id, kind, planned_min: plannedMin, at: now.toISOString(), source: 'cli' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function endRecord(session, now, note = '') {
|
|
136
|
+
const done = session.status !== 'paused' && now.getTime() >= Date.parse(session.planned_end_at);
|
|
137
|
+
const record = {
|
|
138
|
+
v: 1,
|
|
139
|
+
type: 'end',
|
|
140
|
+
id: session.id,
|
|
141
|
+
status: done ? 'done' : 'partial',
|
|
142
|
+
at: done ? session.planned_end_at : now.toISOString(),
|
|
143
|
+
source: 'cli',
|
|
144
|
+
};
|
|
145
|
+
const oneLine = String(note).replace(/\s+/g, ' ').trim();
|
|
146
|
+
if (oneLine) record.note = oneLine;
|
|
147
|
+
return record;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function automaticEndRecord(session, now) {
|
|
151
|
+
if (!['running', 'paused'].includes(session.status)) return null;
|
|
152
|
+
const expired = session.pause_expired_at || (session.status === 'paused'
|
|
153
|
+
&& now.getTime() - Date.parse(session.paused_at) > PAUSE_LIMIT_MS ? session.paused_at : null);
|
|
154
|
+
if (expired) {
|
|
155
|
+
return { v: 1, type: 'end', id: session.id, status: 'partial', at: expired, source: 'cli', inferred: true };
|
|
156
|
+
}
|
|
157
|
+
if (session.status === 'running' && now.getTime() >= Date.parse(session.planned_end_at)) {
|
|
158
|
+
return { ...endRecord(session, now), inferred: true };
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function pauseRecord(session, now) {
|
|
164
|
+
if (!session || automaticEndRecord(session, now)) throw new Error('nothing running');
|
|
165
|
+
if (session.status === 'paused') throw new Error('session is already paused; use atris 99 resume');
|
|
166
|
+
if (session.status !== 'running') throw new Error('nothing running');
|
|
167
|
+
return { v: 1, type: 'pause', id: session.id, at: now.toISOString(), source: 'cli' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resumeRecord(session, now) {
|
|
171
|
+
if (!session || automaticEndRecord(session, now)) throw new Error('nothing paused');
|
|
172
|
+
if (session.status !== 'paused') throw new Error('session is not paused');
|
|
173
|
+
return { v: 1, type: 'resume', id: session.id, at: now.toISOString(), source: 'cli' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function activeMilliseconds(session, now) {
|
|
177
|
+
const end = session.ended_at ? Date.parse(session.ended_at) : now.getTime();
|
|
178
|
+
const until = session.paused_at ? Math.min(end, Date.parse(session.paused_at)) : end;
|
|
179
|
+
return Math.max(0, until - Date.parse(session.started_at) - (session.paused_ms || 0));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sessionStatus(session, now) {
|
|
183
|
+
return {
|
|
184
|
+
running: session ? {
|
|
185
|
+
...session,
|
|
186
|
+
remaining_seconds: Math.max(0, Math.ceil((session.planned_min * 60000 - activeMilliseconds(session, now)) / 1000)),
|
|
187
|
+
} : null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function localDate(date) {
|
|
192
|
+
return [date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0')].join('-');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sessionsToday(sessions, now) {
|
|
196
|
+
const date = localDate(now);
|
|
197
|
+
const done = sessions.filter((session) => session.status === 'done'
|
|
198
|
+
&& localDate(new Date(session.ended_at)) === date);
|
|
199
|
+
return {
|
|
200
|
+
date,
|
|
201
|
+
sessions: sessions.filter((session) => localDate(new Date(session.started_at)) === date
|
|
202
|
+
|| (session.ended_at && localDate(new Date(session.ended_at)) === date)),
|
|
203
|
+
finished_minutes: done.reduce((minutes, session) => minutes + session.planned_min, 0),
|
|
204
|
+
counts: {
|
|
205
|
+
focus: done.filter((session) => session.kind === 'focus').length,
|
|
206
|
+
meditate: done.filter((session) => session.kind === 'meditate').length,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function healthToday(records, now) {
|
|
212
|
+
const date = localDate(now);
|
|
213
|
+
let latest;
|
|
214
|
+
for (const record of records) {
|
|
215
|
+
if (record.type === 'health_day' && record.date === date
|
|
216
|
+
&& (!latest || Date.parse(record.at) >= Date.parse(latest.at))) latest = record;
|
|
217
|
+
}
|
|
218
|
+
return latest;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function healthSummary(record) {
|
|
222
|
+
const parts = [];
|
|
223
|
+
if (record.sleep_min !== undefined) parts.push(`sleep ${Math.floor(record.sleep_min / 60)}h ${record.sleep_min % 60}m`);
|
|
224
|
+
if (record.steps !== undefined) parts.push(`${record.steps.toLocaleString('en-US')} steps`);
|
|
225
|
+
if (record.workout_min !== undefined) parts.push(`workout ${record.workout_min} min`);
|
|
226
|
+
if (record.mindful_min !== undefined) parts.push(`mindful ${record.mindful_min} min`);
|
|
227
|
+
return `Health: ${parts.join(', ') || 'unknown'}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
validLength,
|
|
232
|
+
parseSessionLog,
|
|
233
|
+
foldSessions,
|
|
234
|
+
runningSession,
|
|
235
|
+
startRecord,
|
|
236
|
+
endRecord,
|
|
237
|
+
automaticEndRecord,
|
|
238
|
+
pauseRecord,
|
|
239
|
+
resumeRecord,
|
|
240
|
+
activeMilliseconds,
|
|
241
|
+
sessionStatus,
|
|
242
|
+
sessionsToday,
|
|
243
|
+
healthToday,
|
|
244
|
+
healthSummary,
|
|
245
|
+
lineIdentity,
|
|
246
|
+
uniqueLines,
|
|
247
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const PACKAGE_ROOT = path.join(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
const AGENTS_START = '<!-- ATRIS_OPERATING_RULES:START -->';
|
|
9
|
+
const AGENTS_END = '<!-- ATRIS_OPERATING_RULES:END -->';
|
|
10
|
+
|
|
11
|
+
// The first run of bold-lead bullets (`- **Word.** ...`) under
|
|
12
|
+
// `## operating rules` in atris.md. Adapters (AGENTS.md, CLAUDE.md) carry a
|
|
13
|
+
// copy so tools that never open atris.md still see them; atris.md stays the
|
|
14
|
+
// one source. An older atris.md has only plain checklist bullets there, so it
|
|
15
|
+
// yields nothing and the caller falls back to the package copy.
|
|
16
|
+
function extractOpeningLines(content) {
|
|
17
|
+
const lines = String(content || '').split(/\r?\n/);
|
|
18
|
+
const start = lines.findIndex((line) => /^## operating rules\s*$/i.test(line));
|
|
19
|
+
if (start === -1) return '';
|
|
20
|
+
const bullets = [];
|
|
21
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
22
|
+
const line = lines[index];
|
|
23
|
+
if (/^##\s+/.test(line)) break;
|
|
24
|
+
if (/^- \*\*[^*]+\.\*\* /.test(line)) bullets.push(line);
|
|
25
|
+
else if (bullets.length && line.trim()) break;
|
|
26
|
+
}
|
|
27
|
+
return bullets.join('\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readOpeningLines(root) {
|
|
31
|
+
for (const file of [path.join(root, 'atris.md'), path.join(root, 'atris', 'atris.md')]) {
|
|
32
|
+
if (!fs.existsSync(file)) continue;
|
|
33
|
+
const found = extractOpeningLines(fs.readFileSync(file, 'utf8'));
|
|
34
|
+
if (found) return found;
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function openingLinesForRoot(root = process.cwd()) {
|
|
40
|
+
return readOpeningLines(root) || readOpeningLines(PACKAGE_ROOT);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderOpeningLinesBlock(bullets, adapter) {
|
|
44
|
+
if (!bullets) return '';
|
|
45
|
+
return `## Operating rules
|
|
46
|
+
|
|
47
|
+
${bullets}
|
|
48
|
+
|
|
49
|
+
Canonical source: \`atris/atris.md\` \`## operating rules\`. This is a copy for tools that only read \`${adapter}\`; if they differ, atris.md wins.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderAgentsOpeningBlock(bullets) {
|
|
53
|
+
const block = renderOpeningLinesBlock(bullets, 'AGENTS.md');
|
|
54
|
+
return block ? `${AGENTS_START}\n${block}\n${AGENTS_END}` : '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Existing AGENTS.md files predate the copy. Replace a marked copy in place;
|
|
58
|
+
// otherwise put it right before `## You translate` (the atris-written layout),
|
|
59
|
+
// before the brain compile block, or at the end.
|
|
60
|
+
function upsertAgentsOpeningLines(filePath, bullets, options = {}) {
|
|
61
|
+
const block = renderAgentsOpeningBlock(bullets);
|
|
62
|
+
const source = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
63
|
+
if (!block) return { action: 'unchanged', content: source };
|
|
64
|
+
let content;
|
|
65
|
+
const start = source.indexOf(AGENTS_START);
|
|
66
|
+
const end = source.indexOf(AGENTS_END, start + AGENTS_START.length);
|
|
67
|
+
if (start !== -1 && end !== -1) {
|
|
68
|
+
content = `${source.slice(0, start)}${block}${source.slice(end + AGENTS_END.length)}`;
|
|
69
|
+
} else {
|
|
70
|
+
const anchor = ['\n## You translate', '\n<!-- ATRIS_BRAIN_COMPILE:START -->']
|
|
71
|
+
.map((marker) => source.indexOf(marker))
|
|
72
|
+
.find((index) => index !== -1);
|
|
73
|
+
if (anchor !== undefined) {
|
|
74
|
+
content = `${source.slice(0, anchor).trimEnd()}\n\n${block}\n${source.slice(anchor)}`;
|
|
75
|
+
} else {
|
|
76
|
+
const prefix = source.trimEnd() || '# AGENTS';
|
|
77
|
+
content = `${prefix}\n\n${block}\n`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const changed = content !== source;
|
|
81
|
+
if (changed && !options.dryRun) {
|
|
82
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
83
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
return { action: source ? (changed ? 'updated' : 'unchanged') : 'created', content };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
extractOpeningLines,
|
|
90
|
+
openingLinesForRoot,
|
|
91
|
+
renderAgentsOpeningBlock,
|
|
92
|
+
renderOpeningLinesBlock,
|
|
93
|
+
upsertAgentsOpeningLines,
|
|
94
|
+
};
|
package/lib/roster-markdown.js
CHANGED
|
@@ -29,12 +29,14 @@ function stripInlineComments(text) {
|
|
|
29
29
|
|
|
30
30
|
// Sections: anything under a top heading, "## jobs", or "## roster" holds
|
|
31
31
|
// one-line job lines; "## team" holds member lines; any other heading is a
|
|
32
|
-
// job whose "- " lines are its workers.
|
|
32
|
+
// job whose "- " lines are its workers. "## picks" holds the owner's short
|
|
33
|
+
// list of models, one "- tool · model" per line. Other lines under a job heading are
|
|
33
34
|
// free notes that are never read as picks.
|
|
34
35
|
function sectionForHeading(level, title) {
|
|
35
36
|
const name = String(title || '').trim().toLowerCase();
|
|
36
37
|
if (level === 1) return 'jobs';
|
|
37
38
|
if (name === 'team') return 'team';
|
|
39
|
+
if (name === 'picks') return 'picks';
|
|
38
40
|
if (name === 'jobs' || name === 'roster') return 'jobs';
|
|
39
41
|
return 'job';
|
|
40
42
|
}
|
|
@@ -77,6 +79,12 @@ function scanRosterMarkdown(text) {
|
|
|
77
79
|
entries.push({ index, lineNumber: index + 1, section, raw: line, name: '', value: body, heading: heading.title, headingIndex: heading.index, dashed, malformed: false });
|
|
78
80
|
return;
|
|
79
81
|
}
|
|
82
|
+
// Under picks, each "- " line is one model on the owner's short list;
|
|
83
|
+
// other lines there are notes.
|
|
84
|
+
if (section === 'picks') {
|
|
85
|
+
if (dashed) entries.push({ index, lineNumber: index + 1, section, raw: line, name: '', value: body, dashed, malformed: false });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
80
88
|
// A "- " bullet under the jobs heading is a note; under team it is a
|
|
81
89
|
// member line.
|
|
82
90
|
if (dashed && section !== 'team') return;
|
|
@@ -194,6 +202,35 @@ function rewriteRosterValues(text, rewrite) {
|
|
|
194
202
|
return joinLines(trimTrailingBlank(lines), scan.newline);
|
|
195
203
|
}
|
|
196
204
|
|
|
205
|
+
// Set one member's line under "## team" to "- member: value": the member's
|
|
206
|
+
// first line is replaced in place (indent and an end note kept); a member
|
|
207
|
+
// with no line is added after the last team line, or under the team heading,
|
|
208
|
+
// or in a new team section at the end. Every other line stays as written.
|
|
209
|
+
function replaceTeamLine(text, member, value) {
|
|
210
|
+
const base = String(text || '').trim() ? String(text) : ROSTER_MARKDOWN_TEMPLATE.join('\n');
|
|
211
|
+
const scan = scanRosterMarkdown(base);
|
|
212
|
+
const lines = [...scan.lines];
|
|
213
|
+
const name = String(member || '').trim().toLowerCase();
|
|
214
|
+
const line = `- ${name}: ${value}`;
|
|
215
|
+
const team = scan.entries.filter((entry) => entry.section === 'team' && !entry.malformed);
|
|
216
|
+
const mine = team.find((entry) => entry.name.trim().toLowerCase() === name);
|
|
217
|
+
if (mine) {
|
|
218
|
+
const indent = /^\s*/.exec(lines[mine.index])[0];
|
|
219
|
+
const note = /\s*(<!--.*?-->)\s*$/.exec(mine.raw);
|
|
220
|
+
lines[mine.index] = `${indent}${line}${note ? ` ${note[1]}` : ''}`;
|
|
221
|
+
return joinLines(trimTrailingBlank(lines), scan.newline);
|
|
222
|
+
}
|
|
223
|
+
const heading = scan.headings.find((entry) => entry.section === 'team');
|
|
224
|
+
if (heading) {
|
|
225
|
+
const next = scan.headings.find((entry) => entry.index > heading.index);
|
|
226
|
+
const under = team.filter((entry) => entry.index > heading.index && (!next || entry.index < next.index));
|
|
227
|
+
lines.splice(under.length ? under[under.length - 1].index + 1 : heading.index + 1, 0, line);
|
|
228
|
+
return joinLines(trimTrailingBlank(lines), scan.newline);
|
|
229
|
+
}
|
|
230
|
+
const trimmed = trimTrailingBlank(lines);
|
|
231
|
+
return joinLines([...trimmed, ...(trimmed.length ? [''] : []), '## team', line], scan.newline);
|
|
232
|
+
}
|
|
233
|
+
|
|
197
234
|
module.exports = {
|
|
198
235
|
OLD_ROSTER_NOTE,
|
|
199
236
|
ROSTER_NOTE,
|
|
@@ -201,6 +238,7 @@ module.exports = {
|
|
|
201
238
|
scanRosterMarkdown,
|
|
202
239
|
jobSections,
|
|
203
240
|
replaceJobSection,
|
|
241
|
+
replaceTeamLine,
|
|
204
242
|
rewriteRosterValues,
|
|
205
243
|
joinLines,
|
|
206
244
|
};
|
package/lib/roster-models.js
CHANGED
|
@@ -124,7 +124,7 @@ function runsDetail(view) {
|
|
|
124
124
|
// keeps a cache of the models it can run; claude takes the names atris knows;
|
|
125
125
|
// every other engine lists its seed models.
|
|
126
126
|
|
|
127
|
-
const KNOWN_CLAUDE_MODELS = Object.freeze(['
|
|
127
|
+
const KNOWN_CLAUDE_MODELS = Object.freeze(['fable 5.1', 'opus 5.5', 'sonnet 5.5', 'haiku 5.5']);
|
|
128
128
|
|
|
129
129
|
// ~/.codex/models_cache.json unless a path is given. Test runs never read the
|
|
130
130
|
// real file unless they point at one on purpose.
|