atris 3.15.13 → 3.15.22
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 +84 -8
- package/README.md +5 -1
- package/atris/AGENTS.md +46 -1
- package/atris/CLAUDE.md +36 -1
- package/atris/GEMINI.md +14 -1
- package/atris/atris.md +12 -1
- package/atris/atrisDev.md +3 -2
- package/atris/context/README.md +11 -0
- package/atris/features/company-brain-sync/validate.md +5 -5
- package/atris/learnings.jsonl +1 -0
- package/atris/policies/atris-design.md +2 -0
- package/atris/skills/aeo/SKILL.md +2 -2
- package/atris/skills/atris/SKILL.md +15 -62
- package/atris/skills/design/SKILL.md +2 -0
- package/atris/skills/imessage/SKILL.md +19 -2
- package/atris/skills/loop/SKILL.md +6 -5
- package/atris/skills/magic-inbox/SKILL.md +1 -1
- package/atris/team/_template/MEMBER.md +23 -1
- package/atris/team/brainstormer/START_HERE.md +6 -0
- package/atris/team/executor/MEMBER.md +13 -0
- package/atris/team/executor/START_HERE.md +6 -0
- package/atris/team/launcher/START_HERE.md +6 -0
- package/atris/team/mission-lead/MEMBER.md +39 -0
- package/atris/team/mission-lead/MISSION.md +33 -0
- package/atris/team/mission-lead/START_HERE.md +6 -0
- package/atris/team/navigator/MEMBER.md +11 -0
- package/atris/team/navigator/START_HERE.md +6 -0
- package/atris/team/opus-overnight/MEMBER.md +39 -0
- package/atris/team/opus-overnight/MISSION.md +61 -0
- package/atris/team/opus-overnight/START_HERE.md +6 -0
- package/atris/team/opus-overnight/STEERING.md +35 -0
- package/atris/team/researcher/START_HERE.md +6 -0
- package/atris/team/validator/MEMBER.md +26 -6
- package/atris/team/validator/START_HERE.md +6 -0
- package/atris/wiki/concepts/agent-activation-contract.md +79 -0
- package/atris/wiki/concepts/workspace-initialization-contract.md +73 -0
- package/atris/wiki/index.md +27 -0
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +17 -0
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +15 -0
- package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +10 -0
- package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +12 -0
- package/atris.md +49 -13
- package/bin/atris.js +660 -22
- package/commands/activate.js +12 -3
- package/commands/aeo.js +1 -1
- package/commands/align.js +10 -10
- package/commands/analytics.js +9 -4
- package/commands/app.js +2 -0
- package/commands/apps.js +276 -0
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +74 -5
- package/commands/brain.js +536 -61
- package/commands/brainstorm.js +12 -12
- package/commands/business-sync.js +142 -24
- package/commands/clean.js +9 -6
- package/commands/codex-goal.js +311 -0
- package/commands/errors.js +11 -1
- package/commands/feedback.js +55 -17
- package/commands/fork.js +2 -2
- package/commands/gm.js +376 -0
- package/commands/init.js +80 -3
- package/commands/integrations.js +524 -0
- package/commands/learn.js +25 -16
- package/commands/lesson.js +41 -0
- package/commands/lifecycle.js +2 -2
- package/commands/member.js +2416 -9
- package/commands/mission.js +1776 -0
- package/commands/now.js +48 -7
- package/commands/play.js +425 -0
- package/commands/publish.js +2 -1
- package/commands/pull.js +72 -29
- package/commands/push.js +199 -17
- package/commands/review.js +51 -13
- package/commands/skill.js +2 -2
- package/commands/soul.js +19 -13
- package/commands/status.js +6 -1
- package/commands/sync.js +5 -4
- package/commands/task.js +1041 -147
- package/commands/terminal.js +5 -5
- package/commands/verify.js +7 -5
- package/commands/visualize.js +7 -0
- package/commands/wiki.js +53 -16
- package/commands/workflow.js +298 -54
- package/commands/workspace-clean.js +1 -1
- package/commands/worktree.js +468 -0
- package/commands/xp.js +1608 -0
- package/lib/manifest.js +34 -4
- package/lib/scorecard.js +3 -2
- package/lib/task-db.js +408 -27
- package/lib/todo-fallback.js +28 -2
- package/lib/todo.js +5 -3
- package/package.json +23 -2
- package/utils/update-check.js +51 -1
package/commands/member.js
CHANGED
|
@@ -1,8 +1,1077 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
3
6
|
const { loadCredentials } = require('../utils/auth');
|
|
4
7
|
const { apiRequestJson } = require('../utils/api');
|
|
5
8
|
|
|
9
|
+
function todayLogName() {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureMemberLog(memberDir, { name, role, description, source = 'cli' } = {}) {
|
|
15
|
+
const logsDir = path.join(memberDir, 'logs');
|
|
16
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
17
|
+
const logPath = path.join(logsDir, todayLogName());
|
|
18
|
+
if (fs.existsSync(logPath)) return logPath;
|
|
19
|
+
const stamp = new Date().toTimeString().slice(0, 5);
|
|
20
|
+
const content = [
|
|
21
|
+
`## ${stamp} · Member initialized`,
|
|
22
|
+
`- team: ${name || path.basename(memberDir)}`,
|
|
23
|
+
role ? `- role: ${role}` : '',
|
|
24
|
+
description ? `- mission: ${description}` : '',
|
|
25
|
+
`- source: ${source}`,
|
|
26
|
+
'- status: ready_for_room',
|
|
27
|
+
'',
|
|
28
|
+
].filter(Boolean).join('\n');
|
|
29
|
+
fs.writeFileSync(logPath, content, 'utf8');
|
|
30
|
+
return logPath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appendMemberLifecycleLog(memberDir, name, action, detail = '') {
|
|
34
|
+
const logsDir = path.join(memberDir, 'logs');
|
|
35
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
36
|
+
const logPath = path.join(logsDir, todayLogName());
|
|
37
|
+
const stamp = new Date().toTimeString().slice(0, 5);
|
|
38
|
+
const content = [
|
|
39
|
+
`## ${stamp} · Member ${action}`,
|
|
40
|
+
`- team: ${name || path.basename(memberDir)}`,
|
|
41
|
+
detail ? `- detail: ${detail}` : '',
|
|
42
|
+
`- status: ${action}`,
|
|
43
|
+
'',
|
|
44
|
+
].filter(Boolean).join('\n');
|
|
45
|
+
fs.appendFileSync(logPath, content, 'utf8');
|
|
46
|
+
return logPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function archiveName(name) {
|
|
50
|
+
return `${name}-${todayLogName().replace(/\.md$/, '')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function uniqueArchiveDir(archiveRoot, name) {
|
|
54
|
+
const base = archiveName(name);
|
|
55
|
+
let candidate = path.join(archiveRoot, base);
|
|
56
|
+
let i = 2;
|
|
57
|
+
while (fs.existsSync(candidate)) {
|
|
58
|
+
candidate = path.join(archiveRoot, `${base}-${i}`);
|
|
59
|
+
i += 1;
|
|
60
|
+
}
|
|
61
|
+
return candidate;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseDaysFlag(flags, fallback = 60) {
|
|
65
|
+
const joined = flags.join(' ');
|
|
66
|
+
const match = joined.match(/--days[=\s]+(\d+)/);
|
|
67
|
+
const days = match ? Number(match[1]) : fallback;
|
|
68
|
+
return Number.isFinite(days) && days >= 0 ? days : fallback;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseConfirmFlag(flags) {
|
|
72
|
+
const joined = flags.join(' ');
|
|
73
|
+
return joined.match(/--confirm[=\s]+["']?([^"']+)["']?/)?.[1] || '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasFlag(args, name) {
|
|
77
|
+
return args.includes(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readFlag(args, name, fallback = '') {
|
|
81
|
+
const prefix = `${name}=`;
|
|
82
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
83
|
+
const arg = args[i];
|
|
84
|
+
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return args[i + 1];
|
|
85
|
+
if (String(arg).startsWith(prefix)) return String(arg).slice(prefix.length).replace(/^["']|["']$/g, '');
|
|
86
|
+
}
|
|
87
|
+
return fallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readNumberFlag(args, name, fallback = null) {
|
|
91
|
+
const raw = readFlag(args, name, '');
|
|
92
|
+
if (!raw) return fallback;
|
|
93
|
+
const value = Number(raw);
|
|
94
|
+
return Number.isFinite(value) ? value : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readRepeatedFlag(args, name) {
|
|
98
|
+
const values = [];
|
|
99
|
+
const prefix = `${name}=`;
|
|
100
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
101
|
+
const arg = String(args[i]);
|
|
102
|
+
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) {
|
|
103
|
+
values.push(String(args[i + 1]).replace(/^["']|["']$/g, ''));
|
|
104
|
+
i += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (arg.startsWith(prefix)) values.push(arg.slice(prefix.length).replace(/^["']|["']$/g, ''));
|
|
108
|
+
}
|
|
109
|
+
return values.filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function stripKnownFlags(args, valueNames, booleanNames = []) {
|
|
113
|
+
const out = [];
|
|
114
|
+
const valueSet = new Set(valueNames);
|
|
115
|
+
const booleanSet = new Set(booleanNames);
|
|
116
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
117
|
+
const arg = String(args[i]);
|
|
118
|
+
const key = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
|
|
119
|
+
if (booleanSet.has(key)) continue;
|
|
120
|
+
if (valueSet.has(key)) {
|
|
121
|
+
if (!arg.includes('=') && args[i + 1] && !String(args[i + 1]).startsWith('--')) i += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
out.push(args[i]);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function stampIso() {
|
|
130
|
+
return new Date().toISOString();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fileSafeStamp() {
|
|
134
|
+
return stampIso().replace(/[:.]/g, '-');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function slugHash(value) {
|
|
138
|
+
return crypto.createHash('sha1').update(String(value || '')).digest('hex').slice(0, 8);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function makeGoalId(title) {
|
|
142
|
+
return `goal-${todayLogName().replace(/\.md$/, '')}-${slugHash(title)}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function makeExperimentId(goalId, title) {
|
|
146
|
+
return `exp-${slugHash(`${goalId}:${title}:${Date.now()}`)}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function memberPaths(name) {
|
|
150
|
+
const teamDir = path.join(process.cwd(), 'atris', 'team');
|
|
151
|
+
const memberDir = path.join(teamDir, name || '');
|
|
152
|
+
return {
|
|
153
|
+
teamDir,
|
|
154
|
+
memberDir,
|
|
155
|
+
memberFile: path.join(memberDir, 'MEMBER.md'),
|
|
156
|
+
missionFile: path.join(memberDir, 'MISSION.md'),
|
|
157
|
+
goalsJson: path.join(memberDir, 'goals.json'),
|
|
158
|
+
goalsMd: path.join(memberDir, 'goals.md'),
|
|
159
|
+
steeringJsonl: path.join(process.cwd(), '.atris', 'state', 'steering.jsonl'),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function missionFileMarkdown({ name, role, description } = {}) {
|
|
164
|
+
const title = role || name || 'Member';
|
|
165
|
+
const purpose = description || `Define why ${name || 'this member'} exists and how it chooses goals.`;
|
|
166
|
+
return [
|
|
167
|
+
'# Mission',
|
|
168
|
+
'',
|
|
169
|
+
'<!-- Human-authored purpose file. Keep this durable; runtime state belongs in .atris/state/*.jsonl and now.md. -->',
|
|
170
|
+
'',
|
|
171
|
+
'## North Star',
|
|
172
|
+
'',
|
|
173
|
+
purpose,
|
|
174
|
+
'',
|
|
175
|
+
'## How To Choose Goals',
|
|
176
|
+
'',
|
|
177
|
+
`- Use MEMBER.md to stay inside ${title}'s identity, authority, and tools.`,
|
|
178
|
+
'- Choose one useful bounded goal toward this mission.',
|
|
179
|
+
'- Verify the work, write the receipt, and update the log.',
|
|
180
|
+
'- Ask the human when vision, taste, risk, or uncertainty matters.',
|
|
181
|
+
'',
|
|
182
|
+
].join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function ensureMissionFile(memberDir, { name, role, description } = {}) {
|
|
186
|
+
const missionPath = path.join(memberDir, 'MISSION.md');
|
|
187
|
+
if (!fs.existsSync(missionPath)) {
|
|
188
|
+
fs.writeFileSync(missionPath, missionFileMarkdown({ name, role, description }), 'utf8');
|
|
189
|
+
}
|
|
190
|
+
return missionPath;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function requireMemberDir(name) {
|
|
194
|
+
if (!name) {
|
|
195
|
+
console.error('Usage: atris member <goal|tick|review> <name> ...');
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
199
|
+
console.error('Member name must be a local slug: letters, numbers, dots, underscores, or dashes.');
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const paths = memberPaths(name);
|
|
203
|
+
if (!fs.existsSync(paths.memberFile)) {
|
|
204
|
+
console.error(`Member "${name}" not found at atris/team/${name}/MEMBER.md`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
return paths;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function emptyMemberGoals(name) {
|
|
211
|
+
return {
|
|
212
|
+
schema: 'atris.member_goals.v1',
|
|
213
|
+
member: name,
|
|
214
|
+
updated_at: stampIso(),
|
|
215
|
+
goals: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function loadMemberGoals(name, paths = memberPaths(name)) {
|
|
220
|
+
if (!fs.existsSync(paths.goalsJson)) return emptyMemberGoals(name);
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(fs.readFileSync(paths.goalsJson, 'utf8'));
|
|
223
|
+
return {
|
|
224
|
+
...emptyMemberGoals(name),
|
|
225
|
+
...parsed,
|
|
226
|
+
goals: Array.isArray(parsed.goals) ? parsed.goals : [],
|
|
227
|
+
};
|
|
228
|
+
} catch {
|
|
229
|
+
return emptyMemberGoals(name);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function activeGoal(goalsState, goalId = '') {
|
|
234
|
+
if (goalId) return goalsState.goals.find((goal) => goal.id === goalId) || null;
|
|
235
|
+
return goalsState.goals.find((goal) => goal.status === 'active') || goalsState.goals[0] || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function allExperiments(state) {
|
|
239
|
+
const out = [];
|
|
240
|
+
for (const goal of state.goals || []) {
|
|
241
|
+
for (const experiment of goal.experiments || []) out.push({ goal, experiment });
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function findExperiment(state, experimentId) {
|
|
247
|
+
for (const item of allExperiments(state)) {
|
|
248
|
+
if (item.experiment.id === experimentId) return item;
|
|
249
|
+
}
|
|
250
|
+
return { goal: null, experiment: null };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function latestByTime(items, field = 'created_at') {
|
|
254
|
+
return items
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.slice()
|
|
257
|
+
.sort((a, b) => String(b[field] || '').localeCompare(String(a[field] || '')))[0] || null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function recentLogLines(memberDir, maxLines = 8) {
|
|
261
|
+
const logsDir = path.join(memberDir, 'logs');
|
|
262
|
+
if (!fs.existsSync(logsDir)) return [];
|
|
263
|
+
const logs = fs.readdirSync(logsDir)
|
|
264
|
+
.filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/.test(name))
|
|
265
|
+
.sort();
|
|
266
|
+
const latest = logs[logs.length - 1];
|
|
267
|
+
if (!latest) return [];
|
|
268
|
+
return fs.readFileSync(path.join(logsDir, latest), 'utf8')
|
|
269
|
+
.split(/\r?\n/)
|
|
270
|
+
.filter((line) => line.trim())
|
|
271
|
+
.slice(-maxLines);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function readOptionalText(filePath) {
|
|
275
|
+
try {
|
|
276
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
277
|
+
} catch {
|
|
278
|
+
return '';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function extractMarkdownSection(text, heading) {
|
|
283
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
284
|
+
const target = String(heading || '').trim().toLowerCase();
|
|
285
|
+
const start = lines.findIndex((line) => {
|
|
286
|
+
const match = line.match(/^##\s+(.+?)\s*$/);
|
|
287
|
+
return match && match[1].trim().toLowerCase() === target;
|
|
288
|
+
});
|
|
289
|
+
if (start === -1) return '';
|
|
290
|
+
const out = [];
|
|
291
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
292
|
+
if (/^##\s+/.test(lines[i])) break;
|
|
293
|
+
out.push(lines[i]);
|
|
294
|
+
}
|
|
295
|
+
return out.join('\n').trim();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function firstUsefulLine(text) {
|
|
299
|
+
return String(text || '')
|
|
300
|
+
.split(/\r?\n/)
|
|
301
|
+
.map((line) => line.replace(/^[-*]\s+/, '').trim())
|
|
302
|
+
.find((line) => line && !line.startsWith('<!--')) || '';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function compactSentence(text, max = 120) {
|
|
306
|
+
const clean = String(text || '').replace(/\s+/g, ' ').trim();
|
|
307
|
+
return clean.length > max ? `${clean.slice(0, Math.max(0, max - 1)).trim()}...` : clean;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function activeRuntimeMissionFromNow(nowText) {
|
|
311
|
+
const heading = String(nowText || '').match(/^##\s+(.+?)\s*$/m)?.[1]?.trim() || '';
|
|
312
|
+
const id = String(nowText || '').match(/^- id:\s*(.+?)\s*$/m)?.[1]?.trim() || '';
|
|
313
|
+
const status = String(nowText || '').match(/^- status:\s*(.+?)\s*$/m)?.[1]?.trim() || '';
|
|
314
|
+
const next = String(nowText || '').match(/^- next:\s*(.+?)\s*$/m)?.[1]?.trim() || '';
|
|
315
|
+
return { heading, id, status, next };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function missionPurpose(paths) {
|
|
319
|
+
const missionText = readOptionalText(paths.missionFile);
|
|
320
|
+
const nowText = readOptionalText(path.join(paths.memberDir, 'now.md'));
|
|
321
|
+
const northStar = firstUsefulLine(extractMarkdownSection(missionText, 'North Star'));
|
|
322
|
+
const goalGuidance = extractMarkdownSection(missionText, 'How To Choose Goals');
|
|
323
|
+
const runtimeMission = activeRuntimeMissionFromNow(nowText);
|
|
324
|
+
const meaningful = Boolean(northStar) && !/define why .* exists/i.test(northStar);
|
|
325
|
+
return {
|
|
326
|
+
missionText,
|
|
327
|
+
nowText,
|
|
328
|
+
northStar,
|
|
329
|
+
goalGuidance,
|
|
330
|
+
runtimeMission,
|
|
331
|
+
meaningful,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function resolveMemberRunMissionId(name, args = []) {
|
|
336
|
+
const override = readFlag(args, '--mission', '') || readFlag(args, '--mission-id', '');
|
|
337
|
+
if (override) return override;
|
|
338
|
+
|
|
339
|
+
const paths = requireMemberDir(name);
|
|
340
|
+
const purpose = missionPurpose(paths);
|
|
341
|
+
if (purpose.runtimeMission?.id) return purpose.runtimeMission.id;
|
|
342
|
+
|
|
343
|
+
const goals = loadMemberGoals(name, paths);
|
|
344
|
+
const goal = activeGoal(goals);
|
|
345
|
+
return goal?.mission_id || '';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function memberRun(name, ...args) {
|
|
349
|
+
if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
350
|
+
console.log('Usage: atris member run <name> [mission run flags]');
|
|
351
|
+
console.log('Example: atris member run block-builder --max-ticks 1 --max-wall 900 --json');
|
|
352
|
+
console.log('Override: atris member run block-builder --mission <mission-id> --json');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const missionId = resolveMemberRunMissionId(name, args);
|
|
357
|
+
if (!missionId) {
|
|
358
|
+
console.error(`No active Mission Runtime found for member "${name}".`);
|
|
359
|
+
console.error(`Try: atris member goal-from-mission ${name} --json`);
|
|
360
|
+
console.error(`Or: atris mission start "..." --owner ${name}`);
|
|
361
|
+
process.exitCode = 1;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const runArgs = stripKnownFlags(args, ['--mission', '--mission-id']);
|
|
366
|
+
if (!readFlag(runArgs, '--max-ticks', '')) runArgs.push('--max-ticks', '1');
|
|
367
|
+
if (!readFlag(runArgs, '--max-wall', '')) runArgs.push('--max-wall', '900');
|
|
368
|
+
|
|
369
|
+
const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
370
|
+
try {
|
|
371
|
+
execFileSync(process.execPath, [cliPath, 'mission', 'run', missionId, ...runArgs], {
|
|
372
|
+
cwd: process.cwd(),
|
|
373
|
+
stdio: 'inherit',
|
|
374
|
+
});
|
|
375
|
+
} catch (error) {
|
|
376
|
+
process.exitCode = Number(error?.status) || 1;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function loadTeamScoreEvidence(scoreJsonPath) {
|
|
381
|
+
const sourcePath = String(scoreJsonPath || '').trim();
|
|
382
|
+
try {
|
|
383
|
+
if (sourcePath) {
|
|
384
|
+
const raw = sourcePath === '-'
|
|
385
|
+
? fs.readFileSync(0, 'utf8')
|
|
386
|
+
: fs.readFileSync(path.resolve(process.cwd(), sourcePath), 'utf8');
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
source: sourcePath === '-' ? 'stdin' : path.relative(process.cwd(), path.resolve(process.cwd(), sourcePath)),
|
|
390
|
+
parsed: JSON.parse(raw),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const scoreScript = path.join(process.cwd(), 'scripts', 'team-overall-score.mjs');
|
|
394
|
+
if (!fs.existsSync(scoreScript)) {
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
source: null,
|
|
398
|
+
error: 'No --score-json was provided and scripts/team-overall-score.mjs was not found.',
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const raw = execFileSync(process.execPath, [scoreScript, '--json'], {
|
|
402
|
+
cwd: process.cwd(),
|
|
403
|
+
encoding: 'utf8',
|
|
404
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
ok: true,
|
|
408
|
+
source: 'scripts/team-overall-score.mjs --json',
|
|
409
|
+
parsed: JSON.parse(raw),
|
|
410
|
+
};
|
|
411
|
+
} catch (error) {
|
|
412
|
+
return {
|
|
413
|
+
ok: false,
|
|
414
|
+
source: sourcePath || null,
|
|
415
|
+
error: error instanceof Error ? error.message : String(error),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function normalizeTeamScoreEvidence(parsed, source) {
|
|
421
|
+
const score = parsed?.score || parsed || {};
|
|
422
|
+
const learningPacket = parsed?.learningPacket || {};
|
|
423
|
+
const dimensions = Array.isArray(score.dimensions) ? score.dimensions : [];
|
|
424
|
+
const weakest = score.weakest || dimensions.slice().sort((a, b) => Number(a.score || 0) - Number(b.score || 0))[0] || null;
|
|
425
|
+
const nextMove = compactSentence(
|
|
426
|
+
score.nextMove
|
|
427
|
+
|| (weakest ? `Raise ${weakest.label || weakest.id || 'Team Overall'}: ${weakest.recommendation || 'Run one verified improvement loop.'}` : ''),
|
|
428
|
+
220,
|
|
429
|
+
);
|
|
430
|
+
if (!nextMove || !weakest) return null;
|
|
431
|
+
const latestReward = parsed?.taskLedger?.latestReward || parsed?.latestReward || null;
|
|
432
|
+
const targetMember = learningPacket.targetMember || parsed?.targetMember || null;
|
|
433
|
+
return {
|
|
434
|
+
source: source || 'unknown',
|
|
435
|
+
overall: Number.isFinite(Number(score.overall)) ? Number(score.overall) : null,
|
|
436
|
+
formula: score.formula || null,
|
|
437
|
+
next_move: nextMove,
|
|
438
|
+
weakest: {
|
|
439
|
+
id: weakest.id || null,
|
|
440
|
+
label: weakest.label || weakest.id || 'Team Overall',
|
|
441
|
+
score: Number.isFinite(Number(weakest.score)) ? Number(weakest.score) : null,
|
|
442
|
+
recommendation: weakest.recommendation || null,
|
|
443
|
+
evidence: weakest.evidence || null,
|
|
444
|
+
},
|
|
445
|
+
latest_reward: latestReward ? {
|
|
446
|
+
ref: latestReward.ref || latestReward.display_id || latestReward.id || null,
|
|
447
|
+
title: latestReward.title || null,
|
|
448
|
+
reward: latestReward.reward == null ? null : Number.isFinite(Number(latestReward.reward)) ? Number(latestReward.reward) : null,
|
|
449
|
+
proof: latestReward.proof || null,
|
|
450
|
+
} : null,
|
|
451
|
+
target_member: targetMember ? {
|
|
452
|
+
slug: targetMember.slug || null,
|
|
453
|
+
label: targetMember.label || targetMember.slug || null,
|
|
454
|
+
overall: Number.isFinite(Number(targetMember.overall)) ? Number(targetMember.overall) : null,
|
|
455
|
+
next: targetMember.next || null,
|
|
456
|
+
weakest_attribute: targetMember.weakestAttribute || targetMember.weakest_attribute || null,
|
|
457
|
+
} : null,
|
|
458
|
+
drill: learningPacket.drill || null,
|
|
459
|
+
verifier: learningPacket.verifier || null,
|
|
460
|
+
generated_at: parsed?.generated_at || parsed?.generatedAt || parsed?.created_at || null,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function latestRewardLine(latestReward) {
|
|
465
|
+
if (!latestReward) return 'no latest reward receipt';
|
|
466
|
+
const ref = latestReward.ref ? `${latestReward.ref} ` : '';
|
|
467
|
+
const reward = latestReward.reward == null ? '' : ` reward ${latestReward.reward}`;
|
|
468
|
+
const proof = latestReward.proof ? ` - ${compactSentence(latestReward.proof, 120)}` : '';
|
|
469
|
+
return `${ref}${latestReward.title || 'latest reviewed task'}${reward}${proof}`.trim();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function readSteeringMemory(paths, name) {
|
|
473
|
+
if (!fs.existsSync(paths.steeringJsonl)) return [];
|
|
474
|
+
const records = [];
|
|
475
|
+
try {
|
|
476
|
+
for (const line of fs.readFileSync(paths.steeringJsonl, 'utf8').split(/\r?\n/)) {
|
|
477
|
+
const trimmed = line.trim();
|
|
478
|
+
if (!trimmed) continue;
|
|
479
|
+
const record = JSON.parse(trimmed);
|
|
480
|
+
if (!record || record.schema !== 'atris.steering.v1' || (record.status || 'active') !== 'active') continue;
|
|
481
|
+
const member = record.scope?.member;
|
|
482
|
+
if (member && member !== name) continue;
|
|
483
|
+
records.push({
|
|
484
|
+
id: record.id,
|
|
485
|
+
kind: record.kind || 'preference',
|
|
486
|
+
created_at: record.created_at || null,
|
|
487
|
+
raw: record.raw || null,
|
|
488
|
+
memory: Array.isArray(record.memory) ? record.memory.filter(Boolean).slice(0, 8) : [],
|
|
489
|
+
anti_patterns: Array.isArray(record.anti_patterns) ? record.anti_patterns.filter(Boolean).slice(0, 8) : [],
|
|
490
|
+
applies_to: Array.isArray(record.applies_to) ? record.applies_to.filter(Boolean).slice(0, 8) : [],
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
return records.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || ''))).slice(0, 12);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const WAKE_DIRECTIVE_DECISIONS = new Set(['close_loop', 'report_proof', 'create_missing_task', 'ask', 'wait']);
|
|
500
|
+
const CLOSED_TASK_STATUSES = new Set(['done', 'complete', 'completed', 'reviewed', 'failed', 'stopped', 'closed', 'cancelled', 'canceled']);
|
|
501
|
+
|
|
502
|
+
function parseWakeDirectiveLine(line) {
|
|
503
|
+
const match = String(line || '').match(/\bwake directive:\s*(close_loop|report_proof|create_missing_task|ask|wait)\b(?:\s*[-:]\s*(.*))?/i);
|
|
504
|
+
if (!match) return null;
|
|
505
|
+
return {
|
|
506
|
+
decision: match[1].toLowerCase(),
|
|
507
|
+
note: compactSentence(match[2] || '', 180),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function commandForWakeDirective(name, directive, goal) {
|
|
512
|
+
const note = directive.note || goal?.title || 'self-improvement loop';
|
|
513
|
+
if (directive.decision === 'close_loop') return `atris task next --json`;
|
|
514
|
+
if (directive.decision === 'report_proof') return `atris task note ${note}`;
|
|
515
|
+
if (directive.decision === 'create_missing_task') return `atris task delegate "${note}" --to ${name} --tag agent`;
|
|
516
|
+
if (directive.decision === 'ask') return `ask: ${note}`;
|
|
517
|
+
return `atris member loop ${name} --status --json`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function taskRefsFromText(text) {
|
|
521
|
+
return [...new Set(String(text || '').match(/\b[A-Z]{2,10}-\d+\b/gi)?.map((ref) => ref.toUpperCase()) || [])];
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function steeringWakeDirective(steering, name, goal) {
|
|
525
|
+
for (const record of steering || []) {
|
|
526
|
+
const lines = [...(record.memory || []), record.raw || ''];
|
|
527
|
+
const task_refs = taskRefsFromText(lines.join('\n'));
|
|
528
|
+
for (const line of lines) {
|
|
529
|
+
const parsed = parseWakeDirectiveLine(line);
|
|
530
|
+
if (!parsed || !WAKE_DIRECTIVE_DECISIONS.has(parsed.decision)) continue;
|
|
531
|
+
return {
|
|
532
|
+
...parsed,
|
|
533
|
+
steering_id: record.id || null,
|
|
534
|
+
task_refs,
|
|
535
|
+
next_command: commandForWakeDirective(name, parsed, goal),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function taskRef(task) {
|
|
543
|
+
return task?.display_id || task?.displayId || task?.legacy_ref || task?.legacyRef || task?.ref || task?.id || null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function lowerCompact(value) {
|
|
547
|
+
return String(value || '').trim().toLowerCase();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function taskCandidateOwnerValues(task) {
|
|
551
|
+
return [
|
|
552
|
+
task?.claimed_by,
|
|
553
|
+
task?.claimedBy,
|
|
554
|
+
task?.assigned_to,
|
|
555
|
+
task?.assignedTo,
|
|
556
|
+
task?.owner,
|
|
557
|
+
task?.metadata?.assigned_to,
|
|
558
|
+
task?.metadata?.assignedTo,
|
|
559
|
+
task?.metadata?.owner,
|
|
560
|
+
task?.atrisContext?.teamMember,
|
|
561
|
+
].filter(Boolean).map(lowerCompact);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function taskBelongsToMember(task, name) {
|
|
565
|
+
return taskCandidateOwnerValues(task).includes(lowerCompact(name));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function taskHasReviewProof(task) {
|
|
569
|
+
if (task?.review?.proof) return true;
|
|
570
|
+
if (task?.proof) return true;
|
|
571
|
+
return (task?.events || []).some((event) => event?.payload?.proof || event?.payload?.review?.proof);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function taskCandidateFromSource(task, source, sourcePath = '') {
|
|
575
|
+
const ref = taskRef(task);
|
|
576
|
+
if (!ref) return null;
|
|
577
|
+
const status = lowerCompact(task.status || task.state || 'open');
|
|
578
|
+
const title = compactSentence(task.title || task.summary || ref, 120);
|
|
579
|
+
const base = {
|
|
580
|
+
source,
|
|
581
|
+
source_path: sourcePath || null,
|
|
582
|
+
task_ref: ref,
|
|
583
|
+
title,
|
|
584
|
+
status,
|
|
585
|
+
claimed_by: task.claimed_by || task.claimedBy || null,
|
|
586
|
+
assigned_to: task.assigned_to || task.assignedTo || task.owner || task.metadata?.assigned_to || task.metadata?.owner || null,
|
|
587
|
+
proof: task.review?.proof || task.proof || null,
|
|
588
|
+
updated_at: task.updated_at || task.updatedAt || task.done_at || task.created_at || task.createdAt || null,
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if (['blocked', 'needs_you', 'needs-user', 'needs_user'].includes(status)) {
|
|
592
|
+
return {
|
|
593
|
+
...base,
|
|
594
|
+
decision: 'ask',
|
|
595
|
+
ask: compactSentence(task.blocker || task.block?.ask || task.review?.next_task || `Need operator input for ${ref}.`, 180),
|
|
596
|
+
next_command: `atris task show ${ref} --json`,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (['open', 'backlog', 'claimed', 'in_progress', 'in-progress', 'working', 'plan', 'do', 'review', 'ready'].includes(status)) {
|
|
601
|
+
const alreadyClaimedByMember = lowerCompact(base.claimed_by) === lowerCompact(base.assigned_to);
|
|
602
|
+
return {
|
|
603
|
+
...base,
|
|
604
|
+
decision: 'close_loop',
|
|
605
|
+
ask: null,
|
|
606
|
+
next_command: alreadyClaimedByMember
|
|
607
|
+
? `atris task note ${ref} "Closing nearest open loop: ${title}"`
|
|
608
|
+
: `atris task claim ${ref} --as ${base.assigned_to || 'member'}`,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (status === 'done' && !taskHasReviewProof(task)) {
|
|
613
|
+
return {
|
|
614
|
+
...base,
|
|
615
|
+
decision: 'report_proof',
|
|
616
|
+
ask: null,
|
|
617
|
+
next_command: `atris task note ${ref} "Report proof for completed loop: ${title}"`,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function taskIsClosed(task) {
|
|
625
|
+
if (!task) return false;
|
|
626
|
+
const status = lowerCompact(task.status || task.state || '');
|
|
627
|
+
if (CLOSED_TASK_STATUSES.has(status)) return true;
|
|
628
|
+
return Boolean(task.done_at || task.doneAt) && !['open', 'backlog', 'claimed', 'in_progress', 'in-progress', 'working', 'plan', 'do', 'review', 'ready'].includes(status);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function taskProjectionRows() {
|
|
632
|
+
const projectionPath = path.join(process.cwd(), '.atris', 'state', 'tasks.projection.json');
|
|
633
|
+
const projection = readJsonIfExists(projectionPath);
|
|
634
|
+
return Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function findProjectionTaskByRef(ref) {
|
|
638
|
+
const wanted = String(ref || '').toUpperCase();
|
|
639
|
+
return taskProjectionRows().find((task) => String(taskRef(task) || '').toUpperCase() === wanted) || null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function readTaskShowByRef(ref) {
|
|
643
|
+
try {
|
|
644
|
+
const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
645
|
+
const output = execFileSync(process.execPath, [cliPath, 'task', 'show', ref, '--json'], {
|
|
646
|
+
cwd: process.cwd(),
|
|
647
|
+
encoding: 'utf8',
|
|
648
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
649
|
+
timeout: 5000,
|
|
650
|
+
});
|
|
651
|
+
const parsed = JSON.parse(output || '{}');
|
|
652
|
+
return parsed?.task || parsed || null;
|
|
653
|
+
} catch {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function resolveTaskRefStatus(ref) {
|
|
659
|
+
const projectionTask = findProjectionTaskByRef(ref);
|
|
660
|
+
if (projectionTask) {
|
|
661
|
+
return {
|
|
662
|
+
ref,
|
|
663
|
+
found: true,
|
|
664
|
+
source: 'task_projection',
|
|
665
|
+
status: projectionTask.status || projectionTask.state || null,
|
|
666
|
+
closed: taskIsClosed(projectionTask),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const shownTask = readTaskShowByRef(ref);
|
|
670
|
+
if (shownTask) {
|
|
671
|
+
return {
|
|
672
|
+
ref,
|
|
673
|
+
found: true,
|
|
674
|
+
source: 'task_show',
|
|
675
|
+
status: shownTask.status || shownTask.state || null,
|
|
676
|
+
closed: taskIsClosed(shownTask),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
ref,
|
|
681
|
+
found: false,
|
|
682
|
+
source: null,
|
|
683
|
+
status: null,
|
|
684
|
+
closed: false,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function steeringDirectiveClosure(directive) {
|
|
689
|
+
const refs = Array.isArray(directive?.task_refs) ? directive.task_refs : [];
|
|
690
|
+
const tasks = refs.map(resolveTaskRefStatus);
|
|
691
|
+
const missing_refs = tasks.filter((task) => !task.found).map((task) => task.ref);
|
|
692
|
+
const open_refs = tasks.filter((task) => task.found && !task.closed).map((task) => task.ref);
|
|
693
|
+
const closed_refs = tasks.filter((task) => task.found && task.closed).map((task) => task.ref);
|
|
694
|
+
return {
|
|
695
|
+
steering_id: directive?.steering_id || null,
|
|
696
|
+
task_refs: refs,
|
|
697
|
+
tasks,
|
|
698
|
+
closed_refs,
|
|
699
|
+
open_refs,
|
|
700
|
+
missing_refs,
|
|
701
|
+
all_closed: refs.length > 0 && open_refs.length === 0 && missing_refs.length === 0,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function candidatePriority(candidate) {
|
|
706
|
+
const decisionPriority = {
|
|
707
|
+
ask: 4,
|
|
708
|
+
close_loop: 3,
|
|
709
|
+
report_proof: 2,
|
|
710
|
+
create_missing_task: 1,
|
|
711
|
+
}[candidate?.decision] || 0;
|
|
712
|
+
const sourcePriority = {
|
|
713
|
+
task_projection: 3,
|
|
714
|
+
member_room: 2,
|
|
715
|
+
member_room_unlinked_request: 2,
|
|
716
|
+
}[candidate?.source] || 0;
|
|
717
|
+
return decisionPriority * 10 + sourcePriority;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function sortEvidenceCandidates(candidates) {
|
|
721
|
+
return candidates
|
|
722
|
+
.filter(Boolean)
|
|
723
|
+
.slice()
|
|
724
|
+
.sort((a, b) => {
|
|
725
|
+
const byPriority = candidatePriority(b) - candidatePriority(a);
|
|
726
|
+
if (byPriority) return byPriority;
|
|
727
|
+
return String(b.updated_at || '').localeCompare(String(a.updated_at || ''));
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function readTaskProjectionEvidence(name) {
|
|
732
|
+
const projectionPath = path.join(process.cwd(), '.atris', 'state', 'tasks.projection.json');
|
|
733
|
+
const projection = readJsonIfExists(projectionPath);
|
|
734
|
+
const tasks = taskProjectionRows();
|
|
735
|
+
const candidates = sortEvidenceCandidates(
|
|
736
|
+
tasks
|
|
737
|
+
.filter((task) => taskBelongsToMember(task, name))
|
|
738
|
+
.map((task) => taskCandidateFromSource(task, 'task_projection', projectionPath)),
|
|
739
|
+
);
|
|
740
|
+
return {
|
|
741
|
+
path: projectionPath,
|
|
742
|
+
exists: Boolean(projection),
|
|
743
|
+
task_count: tasks.length,
|
|
744
|
+
candidate_count: candidates.length,
|
|
745
|
+
nearest: candidates[0] || null,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function listThreadJsonFiles(root) {
|
|
750
|
+
if (!root || !fs.existsSync(root)) return [];
|
|
751
|
+
const out = [];
|
|
752
|
+
try {
|
|
753
|
+
for (const entry of fs.readdirSync(root)) {
|
|
754
|
+
const projectPath = path.join(root, entry);
|
|
755
|
+
let stat = null;
|
|
756
|
+
try {
|
|
757
|
+
stat = fs.statSync(projectPath);
|
|
758
|
+
} catch {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (stat.isFile() && entry.endsWith('.json')) {
|
|
762
|
+
out.push({ path: projectPath, mtimeMs: stat.mtimeMs });
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (!stat.isDirectory()) continue;
|
|
766
|
+
for (const file of fs.readdirSync(projectPath)) {
|
|
767
|
+
if (!file.endsWith('.json')) continue;
|
|
768
|
+
const fullPath = path.join(projectPath, file);
|
|
769
|
+
try {
|
|
770
|
+
const fileStat = fs.statSync(fullPath);
|
|
771
|
+
out.push({ path: fullPath, mtimeMs: fileStat.mtimeMs });
|
|
772
|
+
} catch {
|
|
773
|
+
// ignore unreadable thread files
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
} catch {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
return out.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 80);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function latestActionableUserLine(thread) {
|
|
784
|
+
const messages = Array.isArray(thread?.messages) ? thread.messages : [];
|
|
785
|
+
for (const message of messages.slice().reverse()) {
|
|
786
|
+
if (message?.role !== 'user') continue;
|
|
787
|
+
const text = compactSentence(message.text || message.content || '', 140);
|
|
788
|
+
if (!text) continue;
|
|
789
|
+
if (/\b(did you|was it|status|check if|quick check|what happened|what's happening)\b/i.test(text)) continue;
|
|
790
|
+
if (/\b(fix|build|add|wire|prove|ship|close|make|implement|update|create)\b/i.test(text)) return text;
|
|
791
|
+
}
|
|
792
|
+
return '';
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function readMemberRoomEvidence(name) {
|
|
796
|
+
const roots = [path.join(process.cwd(), '.obelisk', 'threads')];
|
|
797
|
+
const projectsPath = path.join(os.homedir(), '.obelisk', 'projects.json');
|
|
798
|
+
const projects = readJsonIfExists(projectsPath);
|
|
799
|
+
if (Array.isArray(projects)) {
|
|
800
|
+
const cwd = path.resolve(process.cwd());
|
|
801
|
+
const project = projects.find((item) => item?.path && path.resolve(item.path) === cwd && item.id);
|
|
802
|
+
if (project) roots.push(path.join(os.homedir(), '.obelisk', 'threads', project.id));
|
|
803
|
+
}
|
|
804
|
+
const seen = new Set();
|
|
805
|
+
const candidates = [];
|
|
806
|
+
let files_checked = 0;
|
|
807
|
+
for (const root of roots) {
|
|
808
|
+
for (const item of listThreadJsonFiles(root)) {
|
|
809
|
+
if (seen.has(item.path)) continue;
|
|
810
|
+
seen.add(item.path);
|
|
811
|
+
const thread = readJsonIfExists(item.path);
|
|
812
|
+
files_checked += 1;
|
|
813
|
+
const context = thread?.atrisContext || {};
|
|
814
|
+
const linkedTasks = Array.isArray(context.linkedTasks) ? context.linkedTasks : [];
|
|
815
|
+
const threadMember = lowerCompact(context.teamMember) === lowerCompact(name);
|
|
816
|
+
for (const linked of linkedTasks) {
|
|
817
|
+
const owned = lowerCompact(linked?.owner || linked?.teamMember || context.teamMember) === lowerCompact(name);
|
|
818
|
+
if (!owned) continue;
|
|
819
|
+
const candidate = taskCandidateFromSource(linked, 'member_room', item.path);
|
|
820
|
+
if (candidate) candidates.push(candidate);
|
|
821
|
+
}
|
|
822
|
+
if (threadMember && linkedTasks.length === 0) {
|
|
823
|
+
const updatedAtMs = Number(thread.updatedAt || thread.updated_at || item.mtimeMs || 0);
|
|
824
|
+
if (updatedAtMs && Date.now() - updatedAtMs > 60 * 60 * 1000) continue;
|
|
825
|
+
const request = latestActionableUserLine(thread);
|
|
826
|
+
if (request) {
|
|
827
|
+
candidates.push({
|
|
828
|
+
source: 'member_room_unlinked_request',
|
|
829
|
+
source_path: item.path,
|
|
830
|
+
task_ref: null,
|
|
831
|
+
title: request,
|
|
832
|
+
status: 'missing_task',
|
|
833
|
+
decision: 'create_missing_task',
|
|
834
|
+
ask: null,
|
|
835
|
+
next_command: `atris task delegate "${request}" --to ${name} --tag agent`,
|
|
836
|
+
updated_at: thread.updatedAt || thread.updated_at || item.mtimeMs,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const sorted = sortEvidenceCandidates(candidates);
|
|
843
|
+
return {
|
|
844
|
+
files_checked,
|
|
845
|
+
candidate_count: sorted.length,
|
|
846
|
+
nearest: sorted[0] || null,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function readRecentWakeReceiptEvidence(name) {
|
|
851
|
+
const latestLoop = readJsonIfExists(memberLoopPaths(name).latestPath);
|
|
852
|
+
const receiptPath = Array.isArray(latestLoop?.tick_receipts)
|
|
853
|
+
? latestLoop.tick_receipts.slice().reverse().find(Boolean)
|
|
854
|
+
: null;
|
|
855
|
+
const latestWake = receiptPath ? readJsonIfExists(receiptPath) : null;
|
|
856
|
+
return {
|
|
857
|
+
latest_loop_path: memberLoopPaths(name).latestPath,
|
|
858
|
+
latest_loop_status: latestLoop?.status || null,
|
|
859
|
+
latest_wake_receipt_path: receiptPath || null,
|
|
860
|
+
latest_wake_decision: latestWake?.decision || null,
|
|
861
|
+
latest_wake_reason: latestWake?.reason || null,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function collectWakeEvidence(name) {
|
|
866
|
+
const taskProjection = readTaskProjectionEvidence(name);
|
|
867
|
+
const memberRoom = readMemberRoomEvidence(name);
|
|
868
|
+
const receipt = readRecentWakeReceiptEvidence(name);
|
|
869
|
+
const nearest = sortEvidenceCandidates([taskProjection.nearest, memberRoom.nearest])[0] || null;
|
|
870
|
+
return {
|
|
871
|
+
task_projection: taskProjection,
|
|
872
|
+
member_room: memberRoom,
|
|
873
|
+
receipt,
|
|
874
|
+
nearest_open_loop: nearest,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function memberValueSummary(state) {
|
|
879
|
+
const reviewed = allExperiments(state)
|
|
880
|
+
.map(({ experiment }) => experiment)
|
|
881
|
+
.filter((experiment) => experiment.status === 'accepted' || experiment.status === 'discarded');
|
|
882
|
+
const scored = reviewed.filter((experiment) => Number.isFinite(Number(experiment.value)));
|
|
883
|
+
const accepted = reviewed.filter((experiment) => experiment.status === 'accepted').length;
|
|
884
|
+
if (!reviewed.length) return { reviewed: 0, accepted: 0, average: null, line: 'No reviewed experiments yet.' };
|
|
885
|
+
const average = scored.length
|
|
886
|
+
? Math.round((scored.reduce((sum, experiment) => sum + Number(experiment.value), 0) / scored.length) * 10) / 10
|
|
887
|
+
: null;
|
|
888
|
+
const scoreLine = average == null ? 'value not scored yet' : `avg value ${average}/5`;
|
|
889
|
+
return { reviewed: reviewed.length, accepted, average, line: `${accepted}/${reviewed.length} accepted; ${scoreLine}.` };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function memberOpenExperiment(state) {
|
|
893
|
+
return latestByTime(allExperiments(state)
|
|
894
|
+
.map(({ goal, experiment }) => ({ ...experiment, goal_id: goal.id, goal_title: goal.title }))
|
|
895
|
+
.filter((experiment) => ['blocked', 'proposed', 'running'].includes(experiment.status)));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function supersedeOtherOpenExperiments(state, activeGoal, proof) {
|
|
899
|
+
const superseded = [];
|
|
900
|
+
for (const goal of state.goals || []) {
|
|
901
|
+
if (goal === activeGoal || goal.id === activeGoal?.id) continue;
|
|
902
|
+
for (const experiment of goal.experiments || []) {
|
|
903
|
+
if (!['proposed', 'running'].includes(experiment.status)) continue;
|
|
904
|
+
experiment.status = 'superseded';
|
|
905
|
+
experiment.superseded_at = stampIso();
|
|
906
|
+
experiment.proof = proof;
|
|
907
|
+
experiment.lesson = 'Direction changed by score-derived goal evidence.';
|
|
908
|
+
experiment.source = experiment.source || 'previous_goal';
|
|
909
|
+
superseded.push({
|
|
910
|
+
goal_id: goal.id,
|
|
911
|
+
goal_title: goal.title,
|
|
912
|
+
experiment_id: experiment.id,
|
|
913
|
+
experiment_title: experiment.title,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return superseded;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function memberLastReviewedExperiment(state) {
|
|
921
|
+
return latestByTime(allExperiments(state)
|
|
922
|
+
.map(({ goal, experiment }) => ({ ...experiment, goal_id: goal.id, goal_title: goal.title }))
|
|
923
|
+
.filter((experiment) => experiment.status === 'accepted' || experiment.status === 'discarded'), 'reviewed_at');
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function renderMemberGoalsMarkdown(state) {
|
|
927
|
+
const lines = [
|
|
928
|
+
'# Goals',
|
|
929
|
+
'',
|
|
930
|
+
'<!-- Generated from goals.json. Edit with `atris member goal/tick/review` when possible. -->',
|
|
931
|
+
'',
|
|
932
|
+
];
|
|
933
|
+
for (const goal of state.goals) {
|
|
934
|
+
lines.push(`## ${goal.title}`);
|
|
935
|
+
lines.push('');
|
|
936
|
+
lines.push(`- id: ${goal.id}`);
|
|
937
|
+
lines.push(`- status: ${goal.status}`);
|
|
938
|
+
lines.push(`- cadence: ${goal.cadence || 'manual'}`);
|
|
939
|
+
if (goal.why) lines.push(`- why: ${goal.why}`);
|
|
940
|
+
const criteria = Array.isArray(goal.acceptance) ? goal.acceptance : [];
|
|
941
|
+
if (criteria.length) {
|
|
942
|
+
lines.push('- acceptance:');
|
|
943
|
+
for (const item of criteria) lines.push(` - ${item}`);
|
|
944
|
+
}
|
|
945
|
+
const experiments = Array.isArray(goal.experiments) ? goal.experiments : [];
|
|
946
|
+
if (experiments.length) {
|
|
947
|
+
lines.push('');
|
|
948
|
+
lines.push('### Experiments');
|
|
949
|
+
for (const experiment of experiments) {
|
|
950
|
+
lines.push(`- ${experiment.id}: ${experiment.status} - ${experiment.title}`);
|
|
951
|
+
if (experiment.proof) lines.push(` - proof: ${experiment.proof}`);
|
|
952
|
+
if (experiment.lesson) lines.push(` - lesson: ${experiment.lesson}`);
|
|
953
|
+
if (Number.isFinite(Number(experiment.value))) lines.push(` - value: ${experiment.value}/5`);
|
|
954
|
+
if (experiment.block?.ask) lines.push(` - ask: ${experiment.block.ask}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
lines.push('');
|
|
958
|
+
}
|
|
959
|
+
if (state.goals.length === 0) lines.push('No goals yet.');
|
|
960
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function writeMemberGoals(paths, state) {
|
|
964
|
+
state.updated_at = stampIso();
|
|
965
|
+
fs.writeFileSync(paths.goalsJson, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
966
|
+
fs.writeFileSync(paths.goalsMd, renderMemberGoalsMarkdown(state), 'utf8');
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function writeWakeReceipt(name, payload) {
|
|
970
|
+
const runsDir = path.join(process.cwd(), 'atris', 'runs');
|
|
971
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
972
|
+
const receiptPath = path.join(runsDir, `member-wake-${name}-${fileSafeStamp()}.json`);
|
|
973
|
+
fs.writeFileSync(receiptPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
974
|
+
return receiptPath;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function memberLoopPaths(name) {
|
|
978
|
+
const stateDir = path.join(process.cwd(), '.atris', 'state', 'member-loops');
|
|
979
|
+
return {
|
|
980
|
+
stateDir,
|
|
981
|
+
lockPath: path.join(stateDir, `${name}.lock.json`),
|
|
982
|
+
stopPath: path.join(stateDir, `${name}.stop.json`),
|
|
983
|
+
latestPath: path.join(stateDir, `${name}.latest.json`),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function writeMemberLoopReceipt(name, payload) {
|
|
988
|
+
const runsDir = path.join(process.cwd(), 'atris', 'runs');
|
|
989
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
990
|
+
const receiptPath = path.join(runsDir, `member-loop-${name}-${fileSafeStamp()}.json`);
|
|
991
|
+
fs.writeFileSync(receiptPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
992
|
+
return receiptPath;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function readJsonIfExists(filePath) {
|
|
996
|
+
try {
|
|
997
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
998
|
+
} catch {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function writeJsonFile(filePath, payload) {
|
|
1004
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1005
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function acquireMemberLoopLease(name, { runId, ttlMs }) {
|
|
1009
|
+
const paths = memberLoopPaths(name);
|
|
1010
|
+
fs.mkdirSync(paths.stateDir, { recursive: true });
|
|
1011
|
+
const nowMs = Date.now();
|
|
1012
|
+
const lease = {
|
|
1013
|
+
schema: 'atris.member_loop_lease.v1',
|
|
1014
|
+
member: name,
|
|
1015
|
+
run_id: runId,
|
|
1016
|
+
pid: process.pid,
|
|
1017
|
+
started_at: stampIso(),
|
|
1018
|
+
heartbeat_at: stampIso(),
|
|
1019
|
+
expires_at_ms: nowMs + ttlMs,
|
|
1020
|
+
};
|
|
1021
|
+
const writeLease = () => {
|
|
1022
|
+
const fd = fs.openSync(paths.lockPath, 'wx');
|
|
1023
|
+
try {
|
|
1024
|
+
fs.writeFileSync(fd, JSON.stringify(lease, null, 2) + '\n', 'utf8');
|
|
1025
|
+
} finally {
|
|
1026
|
+
fs.closeSync(fd);
|
|
1027
|
+
}
|
|
1028
|
+
return { acquired: true, lease, paths, recovered_stale: false };
|
|
1029
|
+
};
|
|
1030
|
+
try {
|
|
1031
|
+
return writeLease();
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
if (error.code !== 'EEXIST') throw error;
|
|
1034
|
+
const active = readJsonIfExists(paths.lockPath);
|
|
1035
|
+
if (active && Number(active.expires_at_ms || 0) <= nowMs) {
|
|
1036
|
+
fs.rmSync(paths.lockPath, { force: true });
|
|
1037
|
+
const result = writeLease();
|
|
1038
|
+
return { ...result, recovered_stale: true, stale_lease: active };
|
|
1039
|
+
}
|
|
1040
|
+
return { acquired: false, lease: active, paths, recovered_stale: false };
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function refreshMemberLoopLease(paths, lease, ttlMs) {
|
|
1045
|
+
writeJsonFile(paths.lockPath, {
|
|
1046
|
+
...lease,
|
|
1047
|
+
heartbeat_at: stampIso(),
|
|
1048
|
+
expires_at_ms: Date.now() + ttlMs,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function releaseMemberLoopLease(paths, lease) {
|
|
1053
|
+
const active = readJsonIfExists(paths.lockPath);
|
|
1054
|
+
if (!active || active.run_id !== lease.run_id || active.pid !== lease.pid) return;
|
|
1055
|
+
fs.rmSync(paths.lockPath, { force: true });
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function appendMemberGoalLog(memberDir, name, title, fields = {}) {
|
|
1059
|
+
const logsDir = path.join(memberDir, 'logs');
|
|
1060
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
1061
|
+
const logPath = path.join(logsDir, todayLogName());
|
|
1062
|
+
const stamp = new Date().toTimeString().slice(0, 5);
|
|
1063
|
+
const rows = [
|
|
1064
|
+
`## ${stamp} · ${title}`,
|
|
1065
|
+
`- team: ${name}`,
|
|
1066
|
+
...Object.entries(fields)
|
|
1067
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
1068
|
+
.map(([key, value]) => `- ${key}: ${String(value).replace(/\n/g, ' ')}`),
|
|
1069
|
+
'',
|
|
1070
|
+
];
|
|
1071
|
+
fs.appendFileSync(logPath, rows.join('\n'), 'utf8');
|
|
1072
|
+
return logPath;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
6
1075
|
// --- YAML Frontmatter Parser (shared with skill.js) ---
|
|
7
1076
|
|
|
8
1077
|
function parseFrontmatter(content) {
|
|
@@ -183,14 +1252,22 @@ function memberList() {
|
|
|
183
1252
|
}
|
|
184
1253
|
|
|
185
1254
|
console.log('');
|
|
186
|
-
console.log(`${members.length} member
|
|
1255
|
+
console.log(`${members.length} ${members.length === 1 ? 'member' : 'members'} found.`);
|
|
187
1256
|
}
|
|
188
1257
|
|
|
189
1258
|
// --- CREATE subcommand ---
|
|
190
1259
|
|
|
1260
|
+
function printMemberCreateUsage(stream = console.log) {
|
|
1261
|
+
stream('Usage: atris member create <name> [--role="Title"] [--description="..."] [--push]');
|
|
1262
|
+
}
|
|
1263
|
+
|
|
191
1264
|
async function memberCreate(name, ...flags) {
|
|
192
|
-
if (
|
|
193
|
-
|
|
1265
|
+
if (name === '--help' || name === '-h' || flags.includes('--help') || flags.includes('-h')) {
|
|
1266
|
+
printMemberCreateUsage();
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (!name || String(name).startsWith('-')) {
|
|
1270
|
+
printMemberCreateUsage(console.error);
|
|
194
1271
|
process.exit(1);
|
|
195
1272
|
}
|
|
196
1273
|
|
|
@@ -230,7 +1307,8 @@ async function memberCreate(name, ...flags) {
|
|
|
230
1307
|
fs.mkdirSync(path.join(memberDir, 'skills'), { recursive: true });
|
|
231
1308
|
fs.mkdirSync(path.join(memberDir, 'tools'), { recursive: true });
|
|
232
1309
|
fs.mkdirSync(path.join(memberDir, 'context'), { recursive: true });
|
|
233
|
-
|
|
1310
|
+
const logPath = ensureMemberLog(memberDir, { name, role, description: description || `Handles ${role.toLowerCase()} tasks` });
|
|
1311
|
+
ensureMissionFile(memberDir, { name, role, description: description || `Handles ${role.toLowerCase()} tasks` });
|
|
234
1312
|
|
|
235
1313
|
const content = `---
|
|
236
1314
|
name: ${name}
|
|
@@ -269,10 +1347,11 @@ tools: []
|
|
|
269
1347
|
|
|
270
1348
|
console.log('');
|
|
271
1349
|
console.log(`✓ Created team/${name}/MEMBER.md`);
|
|
1350
|
+
console.log(`✓ Created team/${name}/MISSION.md`);
|
|
272
1351
|
console.log(`✓ Created team/${name}/skills/`);
|
|
273
1352
|
console.log(`✓ Created team/${name}/tools/`);
|
|
274
1353
|
console.log(`✓ Created team/${name}/context/`);
|
|
275
|
-
console.log(`✓ Created team/${name}/
|
|
1354
|
+
console.log(`✓ Created team/${name}/logs/${path.basename(logPath)}`);
|
|
276
1355
|
|
|
277
1356
|
if (shouldPush) {
|
|
278
1357
|
console.log('');
|
|
@@ -280,6 +1359,7 @@ tools: []
|
|
|
280
1359
|
} else {
|
|
281
1360
|
console.log('');
|
|
282
1361
|
console.log(`Next: edit team/${name}/MEMBER.md to define persona, workflow, and permissions.`);
|
|
1362
|
+
console.log(` edit team/${name}/MISSION.md to define why this member exists.`);
|
|
283
1363
|
console.log(` add skills to team/${name}/skills/<skill-name>/SKILL.md`);
|
|
284
1364
|
console.log(` add context docs to team/${name}/context/`);
|
|
285
1365
|
console.log(` run "atris member push ${name}" to create a cloud agent`);
|
|
@@ -435,11 +1515,12 @@ function memberUpgrade(name) {
|
|
|
435
1515
|
fs.mkdirSync(path.join(memberDir, 'skills'), { recursive: true });
|
|
436
1516
|
fs.mkdirSync(path.join(memberDir, 'tools'), { recursive: true });
|
|
437
1517
|
fs.mkdirSync(path.join(memberDir, 'context'), { recursive: true });
|
|
438
|
-
|
|
1518
|
+
const logPath = ensureMemberLog(memberDir, { name, source: 'upgrade' });
|
|
439
1519
|
fs.renameSync(legacyFile, memberFile);
|
|
1520
|
+
ensureMissionFile(memberDir, { name, description: 'Define why this member exists and how it chooses goals.' });
|
|
440
1521
|
|
|
441
1522
|
console.log(`✓ Upgraded team/${name}.md → team/${name}/MEMBER.md`);
|
|
442
|
-
console.log(`✓ Created skills/, tools/, context/,
|
|
1523
|
+
console.log(`✓ Created MISSION.md, skills/, tools/, context/, logs/${path.basename(logPath)}`);
|
|
443
1524
|
}
|
|
444
1525
|
|
|
445
1526
|
// --- PUSH subcommand ---
|
|
@@ -568,10 +1649,22 @@ async function memberPull(nameOrAgentId) {
|
|
|
568
1649
|
fs.mkdirSync(path.join(memberDir, 'skills'), { recursive: true });
|
|
569
1650
|
fs.mkdirSync(path.join(memberDir, 'tools'), { recursive: true });
|
|
570
1651
|
fs.mkdirSync(path.join(memberDir, 'context'), { recursive: true });
|
|
571
|
-
|
|
1652
|
+
const logPath = ensureMemberLog(memberDir, {
|
|
1653
|
+
name: memberName,
|
|
1654
|
+
role: fm && fm.role,
|
|
1655
|
+
description: fm && fm.description,
|
|
1656
|
+
source: 'pull',
|
|
1657
|
+
});
|
|
1658
|
+
const missionPath = ensureMissionFile(memberDir, {
|
|
1659
|
+
name: memberName,
|
|
1660
|
+
role: fm && fm.role,
|
|
1661
|
+
description: fm && fm.description,
|
|
1662
|
+
});
|
|
572
1663
|
|
|
573
1664
|
fs.writeFileSync(memberFile, content);
|
|
574
1665
|
console.log(`Saved to atris/team/${memberName}/MEMBER.md`);
|
|
1666
|
+
console.log(`Mission ready at atris/team/${memberName}/${path.basename(missionPath)}`);
|
|
1667
|
+
console.log(`Log ready at atris/team/${memberName}/logs/${path.basename(logPath)}`);
|
|
575
1668
|
|
|
576
1669
|
// Sync journal entries
|
|
577
1670
|
const journalResult = await apiRequestJson(`/agent/${agentId}/export-journal`, {
|
|
@@ -585,7 +1678,8 @@ async function memberPull(nameOrAgentId) {
|
|
|
585
1678
|
|
|
586
1679
|
for (const file of journalFiles) {
|
|
587
1680
|
if (!file.path || !file.content) continue;
|
|
588
|
-
const
|
|
1681
|
+
const localJournalPath = String(file.path).replace(/^journal\//, 'logs/');
|
|
1682
|
+
const localPath = path.join(memberDir, localJournalPath);
|
|
589
1683
|
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
590
1684
|
fs.writeFileSync(localPath, file.content);
|
|
591
1685
|
synced++;
|
|
@@ -601,9 +1695,1271 @@ async function memberPull(nameOrAgentId) {
|
|
|
601
1695
|
}
|
|
602
1696
|
}
|
|
603
1697
|
|
|
1698
|
+
function memberArchive(name) {
|
|
1699
|
+
if (!name) {
|
|
1700
|
+
console.error('Usage: atris member archive <name>');
|
|
1701
|
+
process.exit(1);
|
|
1702
|
+
}
|
|
1703
|
+
const teamDir = path.join(process.cwd(), 'atris', 'team');
|
|
1704
|
+
const memberDir = path.join(teamDir, name);
|
|
1705
|
+
const memberFile = path.join(memberDir, 'MEMBER.md');
|
|
1706
|
+
if (!fs.existsSync(memberFile)) {
|
|
1707
|
+
console.error(`Member "${name}" not found at atris/team/${name}/MEMBER.md`);
|
|
1708
|
+
process.exit(1);
|
|
1709
|
+
}
|
|
1710
|
+
const archiveRoot = path.join(teamDir, '_archived');
|
|
1711
|
+
const archiveDir = uniqueArchiveDir(archiveRoot, name);
|
|
1712
|
+
appendMemberLifecycleLog(memberDir, name, 'archived', 'Archived by atris member archive');
|
|
1713
|
+
fs.mkdirSync(archiveRoot, { recursive: true });
|
|
1714
|
+
fs.renameSync(memberDir, archiveDir);
|
|
1715
|
+
console.log(`Archived atris/team/${name} -> ${path.relative(process.cwd(), archiveDir)}`);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function memberPurgeArchived(...flags) {
|
|
1719
|
+
const days = parseDaysFlag(flags, 60);
|
|
1720
|
+
const confirm = parseConfirmFlag(flags);
|
|
1721
|
+
if (confirm !== 'delete archived members') {
|
|
1722
|
+
console.error('Refusing purge. Pass --confirm "delete archived members".');
|
|
1723
|
+
process.exit(1);
|
|
1724
|
+
}
|
|
1725
|
+
const archiveRoot = path.join(process.cwd(), 'atris', 'team', '_archived');
|
|
1726
|
+
if (!fs.existsSync(archiveRoot)) {
|
|
1727
|
+
console.log('No archived members found.');
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
1731
|
+
const entries = fs.readdirSync(archiveRoot, { withFileTypes: true })
|
|
1732
|
+
.filter((entry) => entry.isDirectory())
|
|
1733
|
+
.map((entry) => {
|
|
1734
|
+
const fullPath = path.join(archiveRoot, entry.name);
|
|
1735
|
+
return { name: entry.name, path: fullPath, mtimeMs: fs.statSync(fullPath).mtimeMs };
|
|
1736
|
+
})
|
|
1737
|
+
.filter((entry) => entry.mtimeMs <= cutoff);
|
|
1738
|
+
for (const entry of entries) {
|
|
1739
|
+
fs.rmSync(entry.path, { recursive: true, force: true });
|
|
1740
|
+
console.log(`Purged archived member: ${entry.name}`);
|
|
1741
|
+
}
|
|
1742
|
+
console.log(`Purged ${entries.length} archived member${entries.length === 1 ? '' : 's'} older than ${days} days.`);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function printJsonOrText(payload, lines, asJson) {
|
|
1746
|
+
if (asJson) {
|
|
1747
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
for (const line of lines) console.log(line);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function sleepSync(ms) {
|
|
1754
|
+
const duration = Math.max(0, Number(ms) || 0);
|
|
1755
|
+
if (duration <= 0) return;
|
|
1756
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, duration);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
function memberGoal(name, ...args) {
|
|
1760
|
+
const paths = requireMemberDir(name);
|
|
1761
|
+
const asJson = hasFlag(args, '--json');
|
|
1762
|
+
const acceptance = readRepeatedFlag(args, '--acceptance');
|
|
1763
|
+
const cadence = readFlag(args, '--cadence', 'manual') || 'manual';
|
|
1764
|
+
const why = readFlag(args, '--why', '');
|
|
1765
|
+
const title = stripKnownFlags(args, ['--acceptance', '--cadence', '--why'], ['--json']).join(' ').trim();
|
|
1766
|
+
if (!title) {
|
|
1767
|
+
console.error('Usage: atris member goal <name> "Long-term goal" [--acceptance "..."] [--cadence daily] [--why "..."]');
|
|
1768
|
+
process.exit(1);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const state = loadMemberGoals(name, paths);
|
|
1772
|
+
const id = makeGoalId(title);
|
|
1773
|
+
const existing = state.goals.find((goal) => goal.id === id || goal.title.toLowerCase() === title.toLowerCase());
|
|
1774
|
+
const goal = existing || {
|
|
1775
|
+
id,
|
|
1776
|
+
title,
|
|
1777
|
+
status: 'active',
|
|
1778
|
+
cadence,
|
|
1779
|
+
why,
|
|
1780
|
+
acceptance: acceptance.length ? acceptance : ['Return proof, risk, and next move.'],
|
|
1781
|
+
created_at: stampIso(),
|
|
1782
|
+
experiments: [],
|
|
1783
|
+
history: [],
|
|
1784
|
+
};
|
|
1785
|
+
goal.status = 'active';
|
|
1786
|
+
goal.cadence = cadence || goal.cadence || 'manual';
|
|
1787
|
+
if (why) goal.why = why;
|
|
1788
|
+
if (acceptance.length) goal.acceptance = acceptance;
|
|
1789
|
+
goal.history = Array.isArray(goal.history) ? goal.history : [];
|
|
1790
|
+
goal.history.push({ at: stampIso(), event: existing ? 'goal_updated' : 'goal_created' });
|
|
1791
|
+
if (!existing) state.goals.push(goal);
|
|
1792
|
+
writeMemberGoals(paths, state);
|
|
1793
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, existing ? 'Member goal updated' : 'Member goal created', {
|
|
1794
|
+
goal: goal.title,
|
|
1795
|
+
cadence: goal.cadence,
|
|
1796
|
+
acceptance: (goal.acceptance || []).join(' | '),
|
|
1797
|
+
});
|
|
1798
|
+
printJsonOrText(
|
|
1799
|
+
{ ok: true, action: existing ? 'goal_updated' : 'goal_created', member: name, goal, goals_path: paths.goalsJson, goals_md_path: paths.goalsMd, log_path: logPath },
|
|
1800
|
+
[
|
|
1801
|
+
`${existing ? 'Updated' : 'Created'} goal for ${name}: ${goal.title}`,
|
|
1802
|
+
`Goals: ${path.relative(process.cwd(), paths.goalsJson)}`,
|
|
1803
|
+
`Readout: ${path.relative(process.cwd(), paths.goalsMd)}`,
|
|
1804
|
+
],
|
|
1805
|
+
asJson,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function memberGoalFromMission(name, ...args) {
|
|
1810
|
+
const paths = requireMemberDir(name);
|
|
1811
|
+
const asJson = hasFlag(args, '--json');
|
|
1812
|
+
const force = hasFlag(args, '--force');
|
|
1813
|
+
const cadence = readFlag(args, '--cadence', 'manual') || 'manual';
|
|
1814
|
+
const purpose = missionPurpose(paths);
|
|
1815
|
+
if (!purpose.meaningful) {
|
|
1816
|
+
const ask = `Define atris/team/${name}/MISSION.md with a concrete North Star before this member creates its own goal.`;
|
|
1817
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, 'Member goal-from-mission blocked', {
|
|
1818
|
+
ask,
|
|
1819
|
+
mission_file: path.relative(process.cwd(), paths.missionFile),
|
|
1820
|
+
});
|
|
1821
|
+
printJsonOrText(
|
|
1822
|
+
{ ok: true, action: 'needs_user', member: name, needs_user: true, ask, mission_file: paths.missionFile, log_path: logPath },
|
|
1823
|
+
[
|
|
1824
|
+
`Blocked for ${name}: MISSION.md needs a concrete North Star.`,
|
|
1825
|
+
`Ask: ${ask}`,
|
|
1826
|
+
],
|
|
1827
|
+
asJson,
|
|
1828
|
+
);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const state = loadMemberGoals(name, paths);
|
|
1833
|
+
const runtime = purpose.runtimeMission;
|
|
1834
|
+
const runtimeFocus = runtime.heading || purpose.northStar;
|
|
1835
|
+
const title = `Prove one bounded step toward: ${compactSentence(runtimeFocus, 88)}`;
|
|
1836
|
+
const existing = state.goals.find((goal) => (
|
|
1837
|
+
goal.source === 'mission'
|
|
1838
|
+
&& goal.status === 'active'
|
|
1839
|
+
&& !force
|
|
1840
|
+
&& (
|
|
1841
|
+
goal.mission_id === runtime.id
|
|
1842
|
+
|| String(goal.mission_north_star || '') === purpose.northStar
|
|
1843
|
+
|| goal.title.toLowerCase() === title.toLowerCase()
|
|
1844
|
+
)
|
|
1845
|
+
));
|
|
1846
|
+
const acceptance = [
|
|
1847
|
+
'One bounded next move is proposed from MISSION.md, not hand-fed by the human.',
|
|
1848
|
+
'The move has verifier/proof target, stop rule, and human-ask condition.',
|
|
1849
|
+
'A receipt or log entry records what changed and what remains uncertain.',
|
|
1850
|
+
];
|
|
1851
|
+
const goal = existing || {
|
|
1852
|
+
id: makeGoalId(title),
|
|
1853
|
+
title,
|
|
1854
|
+
status: 'active',
|
|
1855
|
+
cadence,
|
|
1856
|
+
why: compactSentence(purpose.northStar, 240),
|
|
1857
|
+
acceptance,
|
|
1858
|
+
source: 'mission',
|
|
1859
|
+
mission_file: path.relative(process.cwd(), paths.missionFile),
|
|
1860
|
+
now_file: fs.existsSync(path.join(paths.memberDir, 'now.md')) ? path.relative(process.cwd(), path.join(paths.memberDir, 'now.md')) : null,
|
|
1861
|
+
mission_id: runtime.id || null,
|
|
1862
|
+
mission_north_star: purpose.northStar,
|
|
1863
|
+
created_at: stampIso(),
|
|
1864
|
+
experiments: [],
|
|
1865
|
+
history: [],
|
|
1866
|
+
};
|
|
1867
|
+
goal.status = 'active';
|
|
1868
|
+
goal.cadence = cadence || goal.cadence || 'manual';
|
|
1869
|
+
goal.source = 'mission';
|
|
1870
|
+
goal.why = goal.why || compactSentence(purpose.northStar, 240);
|
|
1871
|
+
goal.acceptance = Array.isArray(goal.acceptance) && goal.acceptance.length ? goal.acceptance : acceptance;
|
|
1872
|
+
goal.mission_file = path.relative(process.cwd(), paths.missionFile);
|
|
1873
|
+
goal.now_file = fs.existsSync(path.join(paths.memberDir, 'now.md')) ? path.relative(process.cwd(), path.join(paths.memberDir, 'now.md')) : null;
|
|
1874
|
+
goal.mission_id = runtime.id || goal.mission_id || null;
|
|
1875
|
+
goal.mission_north_star = purpose.northStar;
|
|
1876
|
+
goal.history = Array.isArray(goal.history) ? goal.history : [];
|
|
1877
|
+
goal.history.push({
|
|
1878
|
+
at: stampIso(),
|
|
1879
|
+
event: existing ? 'goal_from_mission_reused' : 'goal_from_mission_created',
|
|
1880
|
+
mission_id: runtime.id || null,
|
|
1881
|
+
mission_status: runtime.status || null,
|
|
1882
|
+
});
|
|
1883
|
+
state.goals = [
|
|
1884
|
+
goal,
|
|
1885
|
+
...state.goals.filter((item) => item.id !== goal.id),
|
|
1886
|
+
];
|
|
1887
|
+
writeMemberGoals(paths, state);
|
|
1888
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, existing ? 'Member goal reused from Mission' : 'Member goal created from Mission', {
|
|
1889
|
+
goal: goal.title,
|
|
1890
|
+
north_star: purpose.northStar,
|
|
1891
|
+
runtime_mission: runtime.id || '',
|
|
1892
|
+
next: `atris member tick ${name} --goal ${goal.id}`,
|
|
1893
|
+
});
|
|
1894
|
+
printJsonOrText(
|
|
1895
|
+
{
|
|
1896
|
+
ok: true,
|
|
1897
|
+
action: existing ? 'goal_from_mission_reused' : 'goal_from_mission_created',
|
|
1898
|
+
member: name,
|
|
1899
|
+
goal,
|
|
1900
|
+
mission: {
|
|
1901
|
+
north_star: purpose.northStar,
|
|
1902
|
+
runtime_id: runtime.id || null,
|
|
1903
|
+
runtime_status: runtime.status || null,
|
|
1904
|
+
runtime_next: runtime.next || null,
|
|
1905
|
+
},
|
|
1906
|
+
goals_path: paths.goalsJson,
|
|
1907
|
+
goals_md_path: paths.goalsMd,
|
|
1908
|
+
log_path: logPath,
|
|
1909
|
+
next_command: `atris member tick ${name} --goal ${goal.id}`,
|
|
1910
|
+
},
|
|
1911
|
+
[
|
|
1912
|
+
`${existing ? 'Reused' : 'Created'} mission-derived goal for ${name}: ${goal.title}`,
|
|
1913
|
+
`Mission: ${purpose.northStar}`,
|
|
1914
|
+
`Next: atris member tick ${name} --goal ${goal.id}`,
|
|
1915
|
+
],
|
|
1916
|
+
asJson,
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function memberGoalFromScore(name, ...args) {
|
|
1921
|
+
const paths = requireMemberDir(name);
|
|
1922
|
+
const asJson = hasFlag(args, '--json');
|
|
1923
|
+
const force = hasFlag(args, '--force');
|
|
1924
|
+
const cadence = readFlag(args, '--cadence', 'manual') || 'manual';
|
|
1925
|
+
const scoreJsonPath = readFlag(args, '--score-json', readFlag(args, '--score', ''));
|
|
1926
|
+
const purpose = missionPurpose(paths);
|
|
1927
|
+
if (!purpose.meaningful) {
|
|
1928
|
+
const ask = `Define atris/team/${name}/MISSION.md with a concrete North Star before this member creates a score-derived goal.`;
|
|
1929
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, 'Member goal-from-score blocked', {
|
|
1930
|
+
ask,
|
|
1931
|
+
mission_file: path.relative(process.cwd(), paths.missionFile),
|
|
1932
|
+
});
|
|
1933
|
+
printJsonOrText(
|
|
1934
|
+
{ ok: true, action: 'needs_user', member: name, needs_user: true, ask, mission_file: paths.missionFile, log_path: logPath },
|
|
1935
|
+
[
|
|
1936
|
+
`Blocked for ${name}: MISSION.md needs a concrete North Star.`,
|
|
1937
|
+
`Ask: ${ask}`,
|
|
1938
|
+
],
|
|
1939
|
+
asJson,
|
|
1940
|
+
);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const loaded = loadTeamScoreEvidence(scoreJsonPath);
|
|
1945
|
+
if (!loaded.ok) {
|
|
1946
|
+
console.error(`Could not load Team score evidence: ${loaded.error || 'unknown error'}`);
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
const scoreEvidence = normalizeTeamScoreEvidence(loaded.parsed, loaded.source);
|
|
1950
|
+
if (!scoreEvidence) {
|
|
1951
|
+
console.error('Team score evidence must include score.nextMove plus a weakest dimension.');
|
|
1952
|
+
process.exit(1);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
const state = loadMemberGoals(name, paths);
|
|
1956
|
+
const title = compactSentence(scoreEvidence.next_move, 120);
|
|
1957
|
+
const goalId = makeGoalId(title);
|
|
1958
|
+
const existing = state.goals.find((goal) => (
|
|
1959
|
+
goal.source === 'team_score'
|
|
1960
|
+
&& goal.status === 'active'
|
|
1961
|
+
&& (
|
|
1962
|
+
goal.id === goalId
|
|
1963
|
+
|| goal.team_score?.next_move === scoreEvidence.next_move
|
|
1964
|
+
|| goal.title.toLowerCase() === title.toLowerCase()
|
|
1965
|
+
)
|
|
1966
|
+
));
|
|
1967
|
+
const acceptance = [
|
|
1968
|
+
`One bounded experiment targets the score-selected next move: ${scoreEvidence.drill || scoreEvidence.next_move}`,
|
|
1969
|
+
`The goal records weakest dimension ${scoreEvidence.weakest.label} and latest reward receipt ${latestRewardLine(scoreEvidence.latest_reward)}.`,
|
|
1970
|
+
scoreEvidence.target_member
|
|
1971
|
+
? `Target member: ${scoreEvidence.target_member.label || scoreEvidence.target_member.slug}${scoreEvidence.target_member.weakest_attribute?.label ? `; weakest attribute: ${scoreEvidence.target_member.weakest_attribute.label}` : ''}.`
|
|
1972
|
+
: 'Target member is recorded when the score packet provides one.',
|
|
1973
|
+
'Review proof or ask the human before replacing this with another score-derived goal.',
|
|
1974
|
+
];
|
|
1975
|
+
const goal = existing || {
|
|
1976
|
+
id: goalId,
|
|
1977
|
+
title,
|
|
1978
|
+
status: 'active',
|
|
1979
|
+
cadence,
|
|
1980
|
+
why: compactSentence(`Team Overall ${scoreEvidence.overall == null ? 'score' : scoreEvidence.overall} selected this from proof: ${scoreEvidence.next_move}`, 240),
|
|
1981
|
+
acceptance,
|
|
1982
|
+
source: 'team_score',
|
|
1983
|
+
mission_file: path.relative(process.cwd(), paths.missionFile),
|
|
1984
|
+
mission_north_star: purpose.northStar,
|
|
1985
|
+
team_score: scoreEvidence,
|
|
1986
|
+
created_at: stampIso(),
|
|
1987
|
+
experiments: [],
|
|
1988
|
+
history: [],
|
|
1989
|
+
};
|
|
1990
|
+
goal.status = 'active';
|
|
1991
|
+
goal.cadence = cadence || goal.cadence || 'manual';
|
|
1992
|
+
goal.source = 'team_score';
|
|
1993
|
+
goal.why = compactSentence(`Team Overall ${scoreEvidence.overall == null ? 'score' : scoreEvidence.overall} selected this from proof: ${scoreEvidence.next_move}`, 240);
|
|
1994
|
+
goal.acceptance = acceptance;
|
|
1995
|
+
goal.mission_file = path.relative(process.cwd(), paths.missionFile);
|
|
1996
|
+
goal.mission_north_star = purpose.northStar;
|
|
1997
|
+
goal.team_score = scoreEvidence;
|
|
1998
|
+
goal.history = Array.isArray(goal.history) ? goal.history : [];
|
|
1999
|
+
goal.history.push({
|
|
2000
|
+
at: stampIso(),
|
|
2001
|
+
event: existing ? 'goal_from_score_reused' : 'goal_from_score_created',
|
|
2002
|
+
score_source: scoreEvidence.source,
|
|
2003
|
+
weakest_dimension: scoreEvidence.weakest.label,
|
|
2004
|
+
latest_reward_ref: scoreEvidence.latest_reward?.ref || null,
|
|
2005
|
+
});
|
|
2006
|
+
if (!existing) state.goals.push(goal);
|
|
2007
|
+
state.goals = [goal, ...state.goals.filter((item) => item !== goal)];
|
|
2008
|
+
const supersedeProof = `Score-derived goal selected ${scoreEvidence.next_move}; superseding older open experiments so the member can change direction.`;
|
|
2009
|
+
const supersededExperiments = supersedeOtherOpenExperiments(state, goal, supersedeProof);
|
|
2010
|
+
writeMemberGoals(paths, state);
|
|
2011
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, existing ? 'Member goal reused from Team score' : 'Member goal created from Team score', {
|
|
2012
|
+
goal: goal.title,
|
|
2013
|
+
score: scoreEvidence.overall == null ? '' : scoreEvidence.overall,
|
|
2014
|
+
weakest: `${scoreEvidence.weakest.label}${scoreEvidence.weakest.score == null ? '' : ` ${scoreEvidence.weakest.score}`}`,
|
|
2015
|
+
latest_reward: latestRewardLine(scoreEvidence.latest_reward),
|
|
2016
|
+
superseded: supersededExperiments.map((item) => item.experiment_id).join(', '),
|
|
2017
|
+
source: scoreEvidence.source,
|
|
2018
|
+
next: `atris member tick ${name} --goal ${goal.id}`,
|
|
2019
|
+
});
|
|
2020
|
+
printJsonOrText(
|
|
2021
|
+
{
|
|
2022
|
+
ok: true,
|
|
2023
|
+
action: existing ? 'goal_from_score_reused' : 'goal_from_score_created',
|
|
2024
|
+
member: name,
|
|
2025
|
+
goal,
|
|
2026
|
+
score: scoreEvidence,
|
|
2027
|
+
superseded_experiments: supersededExperiments,
|
|
2028
|
+
goals_path: paths.goalsJson,
|
|
2029
|
+
goals_md_path: paths.goalsMd,
|
|
2030
|
+
log_path: logPath,
|
|
2031
|
+
next_command: `atris member tick ${name} --goal ${goal.id}`,
|
|
2032
|
+
},
|
|
2033
|
+
[
|
|
2034
|
+
`${existing ? 'Reused' : 'Created'} score-derived goal for ${name}: ${goal.title}`,
|
|
2035
|
+
`Weakest: ${scoreEvidence.weakest.label}${scoreEvidence.weakest.score == null ? '' : ` ${scoreEvidence.weakest.score}`}`,
|
|
2036
|
+
`Latest reward: ${latestRewardLine(scoreEvidence.latest_reward)}`,
|
|
2037
|
+
`Next: atris member tick ${name} --goal ${goal.id}`,
|
|
2038
|
+
],
|
|
2039
|
+
asJson,
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function proposalForGoal(goal) {
|
|
2044
|
+
const criteria = Array.isArray(goal.acceptance) && goal.acceptance.length
|
|
2045
|
+
? goal.acceptance[0]
|
|
2046
|
+
: 'Return proof, risk, and next move.';
|
|
2047
|
+
const scoreEvidence = goal.team_score || null;
|
|
2048
|
+
const target = scoreEvidence?.target_member || null;
|
|
2049
|
+
const drill = scoreEvidence?.drill || null;
|
|
2050
|
+
const title = drill && target
|
|
2051
|
+
? `Run ${target.label || target.slug} drill: ${compactSentence(drill, 96)}`
|
|
2052
|
+
: drill
|
|
2053
|
+
? `Run score drill: ${compactSentence(drill, 108)}`
|
|
2054
|
+
: `Run next proof step for ${goal.title}`;
|
|
2055
|
+
const nextStep = drill
|
|
2056
|
+
? drill
|
|
2057
|
+
: goal.source === 'mission'
|
|
2058
|
+
? `Use ${goal.mission_file || 'MISSION.md'} to produce one receipt-backed bounded proof step for: ${goal.title}`
|
|
2059
|
+
: criteria;
|
|
2060
|
+
return {
|
|
2061
|
+
id: makeExperimentId(goal.id, title),
|
|
2062
|
+
title,
|
|
2063
|
+
status: 'proposed',
|
|
2064
|
+
proof_target: drill ? `Concrete drill: ${drill}` : criteria,
|
|
2065
|
+
next_step: nextStep,
|
|
2066
|
+
target_member: target || null,
|
|
2067
|
+
verifier: scoreEvidence?.verifier || null,
|
|
2068
|
+
stop_rule: 'Stop if proof is missing, risk is unclear, or the next move would require new authority.',
|
|
2069
|
+
created_at: stampIso(),
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function readMemberScope(name, paths) {
|
|
2074
|
+
const scope = [`atris/team/${name}/`];
|
|
2075
|
+
if (paths?.memberFile && fs.existsSync(paths.memberFile)) {
|
|
2076
|
+
try {
|
|
2077
|
+
const fm = parseFrontmatter(fs.readFileSync(paths.memberFile, 'utf8'));
|
|
2078
|
+
if (fm && Array.isArray(fm.scope)) {
|
|
2079
|
+
for (const p of fm.scope) {
|
|
2080
|
+
if (typeof p === 'string' && p.trim()) scope.push(p.trim());
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
} catch { /* ignore — fall through with default scope */ }
|
|
2084
|
+
}
|
|
2085
|
+
return scope;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Auto-generated artifacts that wake/tick produce as side-effects. Dirty here is expected
|
|
2089
|
+
// and must not gate the next wake — otherwise the loop deadlocks on its own log writes.
|
|
2090
|
+
function isAutoGeneratedArtifact(filePath, name) {
|
|
2091
|
+
const memberLogs = `atris/team/${name}/logs/`;
|
|
2092
|
+
if (filePath.startsWith(memberLogs)) return true;
|
|
2093
|
+
if (filePath === `atris/team/${name}/now.md`) return true;
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function porcelainPath(line) {
|
|
2098
|
+
// git status --porcelain entries are "XY path" or "XY old -> new"; we want the post-rename path.
|
|
2099
|
+
const trimmed = String(line || '').slice(3);
|
|
2100
|
+
const arrow = trimmed.indexOf(' -> ');
|
|
2101
|
+
return arrow >= 0 ? trimmed.slice(arrow + 4) : trimmed;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function workspaceSnapshot(name = null, paths = null) {
|
|
2105
|
+
try {
|
|
2106
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
2107
|
+
cwd: process.cwd(),
|
|
2108
|
+
encoding: 'utf8',
|
|
2109
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2110
|
+
}).trim();
|
|
2111
|
+
const porcelain = execFileSync('git', ['status', '--porcelain'], {
|
|
2112
|
+
cwd: process.cwd(),
|
|
2113
|
+
encoding: 'utf8',
|
|
2114
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2115
|
+
}).split(/\r?\n/).filter(Boolean);
|
|
2116
|
+
const memberScope = name ? readMemberScope(name, paths) : [];
|
|
2117
|
+
const dirtyInScope = memberScope.length
|
|
2118
|
+
? porcelain.filter((line) => {
|
|
2119
|
+
const p = porcelainPath(line);
|
|
2120
|
+
if (name && isAutoGeneratedArtifact(p, name)) return false;
|
|
2121
|
+
return memberScope.some((s) => p.startsWith(s));
|
|
2122
|
+
})
|
|
2123
|
+
: [];
|
|
2124
|
+
return {
|
|
2125
|
+
kind: 'git',
|
|
2126
|
+
root,
|
|
2127
|
+
clean: porcelain.length === 0,
|
|
2128
|
+
// Member-scoped clean: dirty files outside the member's scope don't block this member's loop.
|
|
2129
|
+
// Only files inside scope (the member's own lane) gate the wake decision.
|
|
2130
|
+
clean_for_member: dirtyInScope.length === 0,
|
|
2131
|
+
member_scope: memberScope,
|
|
2132
|
+
dirty_count: porcelain.length,
|
|
2133
|
+
dirty_count_in_scope: dirtyInScope.length,
|
|
2134
|
+
dirty_sample: porcelain.slice(0, 8),
|
|
2135
|
+
dirty_in_scope_sample: dirtyInScope.slice(0, 8),
|
|
2136
|
+
};
|
|
2137
|
+
} catch {
|
|
2138
|
+
return {
|
|
2139
|
+
kind: 'none',
|
|
2140
|
+
clean: true,
|
|
2141
|
+
clean_for_member: true,
|
|
2142
|
+
member_scope: [],
|
|
2143
|
+
dirty_count: 0,
|
|
2144
|
+
dirty_count_in_scope: 0,
|
|
2145
|
+
dirty_sample: [],
|
|
2146
|
+
dirty_in_scope_sample: [],
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
function wakeDecision(name, paths, { force = false } = {}) {
|
|
2152
|
+
const purpose = missionPurpose(paths);
|
|
2153
|
+
const steering = readSteeringMemory(paths, name);
|
|
2154
|
+
const state = loadMemberGoals(name, paths);
|
|
2155
|
+
const goal = activeGoal(state);
|
|
2156
|
+
const current = memberOpenExperiment(state);
|
|
2157
|
+
const rawDirective = steeringWakeDirective(steering, name, goal);
|
|
2158
|
+
const directiveClosure = rawDirective ? steeringDirectiveClosure(rawDirective) : null;
|
|
2159
|
+
const directive = directiveClosure?.all_closed ? null : rawDirective;
|
|
2160
|
+
const evidence = collectWakeEvidence(name);
|
|
2161
|
+
evidence.steering_directive_closure = directiveClosure;
|
|
2162
|
+
const blocked = allExperiments(state)
|
|
2163
|
+
.map(({ goal: experimentGoal, experiment }) => ({ ...experiment, goal_id: experimentGoal.id, goal_title: experimentGoal.title }))
|
|
2164
|
+
.filter((experiment) => experiment.status === 'blocked')
|
|
2165
|
+
.sort((a, b) => String(b.blocked_at || b.created_at || '').localeCompare(String(a.blocked_at || a.created_at || '')))[0] || null;
|
|
2166
|
+
const workspace = workspaceSnapshot(name, paths);
|
|
2167
|
+
const checks = {
|
|
2168
|
+
has_member: true,
|
|
2169
|
+
has_mission: Boolean(purpose.missionText),
|
|
2170
|
+
mission_meaningful: purpose.meaningful,
|
|
2171
|
+
has_goal: Boolean(goal),
|
|
2172
|
+
has_open_experiment: Boolean(current),
|
|
2173
|
+
has_blocked_experiment: Boolean(blocked),
|
|
2174
|
+
has_steering: steering.length > 0,
|
|
2175
|
+
has_steering_directive: Boolean(directive),
|
|
2176
|
+
has_satisfied_steering_directive: Boolean(directiveClosure?.all_closed),
|
|
2177
|
+
has_open_loop_evidence: Boolean(evidence.nearest_open_loop),
|
|
2178
|
+
open_loop_source: evidence.nearest_open_loop?.source || null,
|
|
2179
|
+
has_member_room_evidence: Number(evidence.member_room?.candidate_count || 0) > 0,
|
|
2180
|
+
has_recent_receipt: Boolean(evidence.receipt?.latest_wake_receipt_path),
|
|
2181
|
+
workspace_clean: workspace.clean,
|
|
2182
|
+
workspace_clean_for_member: workspace.clean_for_member,
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
if (!purpose.meaningful) {
|
|
2186
|
+
const ask = `Define atris/team/${name}/MISSION.md with a concrete North Star before this member wakes itself.`;
|
|
2187
|
+
return {
|
|
2188
|
+
decision: 'ask',
|
|
2189
|
+
reason: 'mission_missing_or_placeholder',
|
|
2190
|
+
needs_user: true,
|
|
2191
|
+
ask,
|
|
2192
|
+
next_command: `edit atris/team/${name}/MISSION.md`,
|
|
2193
|
+
state,
|
|
2194
|
+
goal: goal || null,
|
|
2195
|
+
current_experiment: current || null,
|
|
2196
|
+
checks,
|
|
2197
|
+
mission: {
|
|
2198
|
+
north_star: purpose.northStar || null,
|
|
2199
|
+
runtime_id: purpose.runtimeMission.id || null,
|
|
2200
|
+
runtime_status: purpose.runtimeMission.status || null,
|
|
2201
|
+
runtime_next: purpose.runtimeMission.next || null,
|
|
2202
|
+
},
|
|
2203
|
+
steering,
|
|
2204
|
+
evidence,
|
|
2205
|
+
workspace,
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
if (!goal) {
|
|
2210
|
+
return {
|
|
2211
|
+
decision: 'stop',
|
|
2212
|
+
reason: 'no_active_goal',
|
|
2213
|
+
needs_user: false,
|
|
2214
|
+
ask: null,
|
|
2215
|
+
next_command: `atris member goal-from-mission ${name}`,
|
|
2216
|
+
state,
|
|
2217
|
+
goal: null,
|
|
2218
|
+
current_experiment: null,
|
|
2219
|
+
checks,
|
|
2220
|
+
mission: {
|
|
2221
|
+
north_star: purpose.northStar,
|
|
2222
|
+
runtime_id: purpose.runtimeMission.id || null,
|
|
2223
|
+
runtime_status: purpose.runtimeMission.status || null,
|
|
2224
|
+
runtime_next: purpose.runtimeMission.next || null,
|
|
2225
|
+
},
|
|
2226
|
+
steering,
|
|
2227
|
+
evidence,
|
|
2228
|
+
workspace,
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
if (blocked) {
|
|
2233
|
+
return {
|
|
2234
|
+
decision: 'ask',
|
|
2235
|
+
reason: 'blocked_experiment',
|
|
2236
|
+
needs_user: true,
|
|
2237
|
+
ask: blocked.block?.ask || 'Needs operator input before another wake.',
|
|
2238
|
+
next_command: `atris member review ${name} ${blocked.id} --discard --proof "..."`,
|
|
2239
|
+
state,
|
|
2240
|
+
goal,
|
|
2241
|
+
current_experiment: blocked,
|
|
2242
|
+
checks,
|
|
2243
|
+
mission: {
|
|
2244
|
+
north_star: purpose.northStar,
|
|
2245
|
+
runtime_id: purpose.runtimeMission.id || null,
|
|
2246
|
+
runtime_status: purpose.runtimeMission.status || null,
|
|
2247
|
+
runtime_next: purpose.runtimeMission.next || null,
|
|
2248
|
+
},
|
|
2249
|
+
steering,
|
|
2250
|
+
evidence,
|
|
2251
|
+
workspace,
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
if (current) {
|
|
2256
|
+
return {
|
|
2257
|
+
decision: 'wait',
|
|
2258
|
+
reason: `open_experiment_${current.status}`,
|
|
2259
|
+
needs_user: false,
|
|
2260
|
+
ask: null,
|
|
2261
|
+
next_command: `atris member review ${name} ${current.id} --accept --proof "..." --value 4`,
|
|
2262
|
+
state,
|
|
2263
|
+
goal,
|
|
2264
|
+
current_experiment: current,
|
|
2265
|
+
checks,
|
|
2266
|
+
mission: {
|
|
2267
|
+
north_star: purpose.northStar,
|
|
2268
|
+
runtime_id: purpose.runtimeMission.id || null,
|
|
2269
|
+
runtime_status: purpose.runtimeMission.status || null,
|
|
2270
|
+
runtime_next: purpose.runtimeMission.next || null,
|
|
2271
|
+
},
|
|
2272
|
+
steering,
|
|
2273
|
+
evidence,
|
|
2274
|
+
workspace,
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
if (evidence.nearest_open_loop) {
|
|
2279
|
+
const openLoop = evidence.nearest_open_loop;
|
|
2280
|
+
const needsUser = openLoop.decision === 'ask';
|
|
2281
|
+
const evidenceRef = openLoop.task_ref || 'missing_task';
|
|
2282
|
+
return {
|
|
2283
|
+
decision: openLoop.decision,
|
|
2284
|
+
reason: `nearest_open_loop:${openLoop.source}:${evidenceRef}`,
|
|
2285
|
+
needs_user: needsUser,
|
|
2286
|
+
ask: needsUser ? (openLoop.ask || `Need operator input for ${openLoop.title}.`) : null,
|
|
2287
|
+
next_command: openLoop.next_command,
|
|
2288
|
+
state,
|
|
2289
|
+
goal,
|
|
2290
|
+
current_experiment: null,
|
|
2291
|
+
checks,
|
|
2292
|
+
mission: {
|
|
2293
|
+
north_star: purpose.northStar,
|
|
2294
|
+
runtime_id: purpose.runtimeMission.id || null,
|
|
2295
|
+
runtime_status: purpose.runtimeMission.status || null,
|
|
2296
|
+
runtime_next: purpose.runtimeMission.next || null,
|
|
2297
|
+
},
|
|
2298
|
+
steering,
|
|
2299
|
+
evidence,
|
|
2300
|
+
workspace,
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
if (directive) {
|
|
2305
|
+
const needsUser = directive.decision === 'ask';
|
|
2306
|
+
return {
|
|
2307
|
+
decision: directive.decision,
|
|
2308
|
+
reason: `steering_directive:${directive.steering_id || 'unknown'}`,
|
|
2309
|
+
needs_user: needsUser,
|
|
2310
|
+
ask: needsUser ? (directive.note || 'Needs operator direction.') : null,
|
|
2311
|
+
next_command: directive.next_command,
|
|
2312
|
+
state,
|
|
2313
|
+
goal,
|
|
2314
|
+
current_experiment: null,
|
|
2315
|
+
checks,
|
|
2316
|
+
mission: {
|
|
2317
|
+
north_star: purpose.northStar,
|
|
2318
|
+
runtime_id: purpose.runtimeMission.id || null,
|
|
2319
|
+
runtime_status: purpose.runtimeMission.status || null,
|
|
2320
|
+
runtime_next: purpose.runtimeMission.next || null,
|
|
2321
|
+
},
|
|
2322
|
+
steering,
|
|
2323
|
+
evidence,
|
|
2324
|
+
workspace,
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
if (!workspace.clean_for_member && !force) {
|
|
2329
|
+
return {
|
|
2330
|
+
decision: 'wait',
|
|
2331
|
+
reason: 'workspace_dirty_in_member_scope',
|
|
2332
|
+
needs_user: false,
|
|
2333
|
+
ask: null,
|
|
2334
|
+
next_command: `commit/stash files in atris/team/${name}/ (or member scope) — or rerun: atris member wake ${name} --force`,
|
|
2335
|
+
state,
|
|
2336
|
+
goal,
|
|
2337
|
+
current_experiment: null,
|
|
2338
|
+
checks,
|
|
2339
|
+
mission: {
|
|
2340
|
+
north_star: purpose.northStar,
|
|
2341
|
+
runtime_id: purpose.runtimeMission.id || null,
|
|
2342
|
+
runtime_status: purpose.runtimeMission.status || null,
|
|
2343
|
+
runtime_next: purpose.runtimeMission.next || null,
|
|
2344
|
+
},
|
|
2345
|
+
steering,
|
|
2346
|
+
evidence,
|
|
2347
|
+
workspace,
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
return {
|
|
2352
|
+
decision: 'tick',
|
|
2353
|
+
reason: 'safe_next_bounded_step',
|
|
2354
|
+
needs_user: false,
|
|
2355
|
+
ask: null,
|
|
2356
|
+
next_command: `atris member tick ${name} --goal ${goal.id}`,
|
|
2357
|
+
state,
|
|
2358
|
+
goal,
|
|
2359
|
+
current_experiment: null,
|
|
2360
|
+
checks,
|
|
2361
|
+
mission: {
|
|
2362
|
+
north_star: purpose.northStar,
|
|
2363
|
+
runtime_id: purpose.runtimeMission.id || null,
|
|
2364
|
+
runtime_status: purpose.runtimeMission.status || null,
|
|
2365
|
+
runtime_next: purpose.runtimeMission.next || null,
|
|
2366
|
+
},
|
|
2367
|
+
steering,
|
|
2368
|
+
evidence,
|
|
2369
|
+
workspace,
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function runMemberWake(name, { execute = false, confirmed = false, force = false } = {}) {
|
|
2374
|
+
const paths = requireMemberDir(name);
|
|
2375
|
+
const planned = wakeDecision(name, paths, { force });
|
|
2376
|
+
const mode = execute ? 'execute' : 'dry_run';
|
|
2377
|
+
let decision = planned.decision;
|
|
2378
|
+
let reason = planned.reason;
|
|
2379
|
+
let executed = false;
|
|
2380
|
+
let experiment = null;
|
|
2381
|
+
let state = planned.state;
|
|
2382
|
+
let goal = planned.goal;
|
|
2383
|
+
let nextCommand = planned.next_command;
|
|
2384
|
+
const now = stampIso();
|
|
2385
|
+
|
|
2386
|
+
if (execute && !confirmed) {
|
|
2387
|
+
decision = 'stop';
|
|
2388
|
+
reason = 'execute_requires_confirm_autonomy_policy';
|
|
2389
|
+
nextCommand = `atris member wake ${name} --execute --confirm-autonomy-policy`;
|
|
2390
|
+
} else if (execute && planned.decision === 'tick' && goal) {
|
|
2391
|
+
goal.experiments = Array.isArray(goal.experiments) ? goal.experiments : [];
|
|
2392
|
+
experiment = proposalForGoal(goal);
|
|
2393
|
+
goal.experiments.push(experiment);
|
|
2394
|
+
goal.history = Array.isArray(goal.history) ? goal.history : [];
|
|
2395
|
+
goal.history.push({ at: now, event: 'wake_tick_proposed_experiment', experiment_id: experiment.id });
|
|
2396
|
+
writeMemberGoals(paths, state);
|
|
2397
|
+
executed = true;
|
|
2398
|
+
decision = 'wait';
|
|
2399
|
+
reason = 'tick_executed_experiment_proposed';
|
|
2400
|
+
nextCommand = `atris member review ${name} ${experiment.id} --accept --proof "..." --value 4`;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
const receiptPayload = {
|
|
2404
|
+
schema: 'atris.member_wake.v1',
|
|
2405
|
+
created_at: now,
|
|
2406
|
+
member: name,
|
|
2407
|
+
mode,
|
|
2408
|
+
decision,
|
|
2409
|
+
reason,
|
|
2410
|
+
executed,
|
|
2411
|
+
needs_user: planned.needs_user || false,
|
|
2412
|
+
ask: planned.ask || null,
|
|
2413
|
+
next_command: nextCommand,
|
|
2414
|
+
mission: planned.mission,
|
|
2415
|
+
steering: planned.steering,
|
|
2416
|
+
evidence: planned.evidence,
|
|
2417
|
+
checks: planned.checks,
|
|
2418
|
+
workspace: planned.workspace,
|
|
2419
|
+
active_goal: goal ? {
|
|
2420
|
+
id: goal.id,
|
|
2421
|
+
title: goal.title,
|
|
2422
|
+
source: goal.source || null,
|
|
2423
|
+
mission_id: goal.mission_id || null,
|
|
2424
|
+
} : null,
|
|
2425
|
+
current_experiment: experiment || planned.current_experiment || null,
|
|
2426
|
+
};
|
|
2427
|
+
const receiptPath = writeWakeReceipt(name, receiptPayload);
|
|
2428
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, executed ? 'Member wake executed tick' : 'Member wake decision', {
|
|
2429
|
+
decision,
|
|
2430
|
+
reason,
|
|
2431
|
+
mode,
|
|
2432
|
+
goal: goal?.title || '',
|
|
2433
|
+
experiment: (experiment || planned.current_experiment)?.title || '',
|
|
2434
|
+
ask: planned.ask || '',
|
|
2435
|
+
receipt: path.relative(process.cwd(), receiptPath),
|
|
2436
|
+
next: nextCommand,
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
return {
|
|
2440
|
+
ok: true,
|
|
2441
|
+
action: 'wake',
|
|
2442
|
+
member: name,
|
|
2443
|
+
mode,
|
|
2444
|
+
decision,
|
|
2445
|
+
reason,
|
|
2446
|
+
executed,
|
|
2447
|
+
needs_user: planned.needs_user || false,
|
|
2448
|
+
ask: planned.ask || null,
|
|
2449
|
+
next_command: nextCommand,
|
|
2450
|
+
mission: planned.mission,
|
|
2451
|
+
steering: planned.steering,
|
|
2452
|
+
evidence: planned.evidence,
|
|
2453
|
+
checks: planned.checks,
|
|
2454
|
+
workspace: planned.workspace,
|
|
2455
|
+
active_goal: receiptPayload.active_goal,
|
|
2456
|
+
current_experiment: receiptPayload.current_experiment,
|
|
2457
|
+
receipt_path: receiptPath,
|
|
2458
|
+
log_path: logPath,
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
function memberWake(name, ...args) {
|
|
2463
|
+
const asJson = hasFlag(args, '--json');
|
|
2464
|
+
const execute = hasFlag(args, '--execute');
|
|
2465
|
+
const confirmed = hasFlag(args, '--confirm-autonomy-policy');
|
|
2466
|
+
const force = hasFlag(args, '--force');
|
|
2467
|
+
const result = runMemberWake(name, { execute, confirmed, force });
|
|
2468
|
+
printJsonOrText(
|
|
2469
|
+
result,
|
|
2470
|
+
[
|
|
2471
|
+
`Wake: ${name}`,
|
|
2472
|
+
`Decision: ${result.decision}`,
|
|
2473
|
+
`Reason: ${result.reason}`,
|
|
2474
|
+
`Mode: ${result.mode}${result.executed ? ' executed' : ''}`,
|
|
2475
|
+
...(result.ask ? [`Ask: ${result.ask}`] : []),
|
|
2476
|
+
`Next: ${result.next_command}`,
|
|
2477
|
+
`Receipt: ${path.relative(process.cwd(), result.receipt_path)}`,
|
|
2478
|
+
],
|
|
2479
|
+
asJson,
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
function memberLoop(name, ...args) {
|
|
2484
|
+
requireMemberDir(name);
|
|
2485
|
+
const asJson = hasFlag(args, '--json');
|
|
2486
|
+
const execute = hasFlag(args, '--execute');
|
|
2487
|
+
const confirmed = hasFlag(args, '--confirm-autonomy-policy');
|
|
2488
|
+
const force = hasFlag(args, '--force');
|
|
2489
|
+
const stop = hasFlag(args, '--stop');
|
|
2490
|
+
const status = hasFlag(args, '--status');
|
|
2491
|
+
const paths = memberLoopPaths(name);
|
|
2492
|
+
fs.mkdirSync(paths.stateDir, { recursive: true });
|
|
2493
|
+
|
|
2494
|
+
if (status) {
|
|
2495
|
+
const active = readJsonIfExists(paths.lockPath);
|
|
2496
|
+
const latest = readJsonIfExists(paths.latestPath);
|
|
2497
|
+
const payload = {
|
|
2498
|
+
ok: true,
|
|
2499
|
+
action: 'loop_status',
|
|
2500
|
+
member: name,
|
|
2501
|
+
active: Boolean(active && Number(active.expires_at_ms || 0) > Date.now()),
|
|
2502
|
+
lease: active || null,
|
|
2503
|
+
latest: latest || null,
|
|
2504
|
+
lock_path: paths.lockPath,
|
|
2505
|
+
latest_path: paths.latestPath,
|
|
2506
|
+
};
|
|
2507
|
+
printJsonOrText(payload, [
|
|
2508
|
+
`Loop status: ${name}`,
|
|
2509
|
+
`Active: ${payload.active ? 'yes' : 'no'}`,
|
|
2510
|
+
`Latest: ${latest?.receipt_path ? path.relative(process.cwd(), latest.receipt_path) : 'none'}`,
|
|
2511
|
+
], asJson);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
if (stop) {
|
|
2516
|
+
const requestedAt = stampIso();
|
|
2517
|
+
const stopPayload = {
|
|
2518
|
+
schema: 'atris.member_loop_stop.v1',
|
|
2519
|
+
member: name,
|
|
2520
|
+
requested_at: requestedAt,
|
|
2521
|
+
pid: process.pid,
|
|
2522
|
+
};
|
|
2523
|
+
writeJsonFile(paths.stopPath, stopPayload);
|
|
2524
|
+
const receiptPath = writeMemberLoopReceipt(name, {
|
|
2525
|
+
ok: true,
|
|
2526
|
+
action: 'loop_stop',
|
|
2527
|
+
member: name,
|
|
2528
|
+
requested_at: requestedAt,
|
|
2529
|
+
stop_path: paths.stopPath,
|
|
2530
|
+
});
|
|
2531
|
+
printJsonOrText({
|
|
2532
|
+
ok: true,
|
|
2533
|
+
action: 'loop_stop',
|
|
2534
|
+
member: name,
|
|
2535
|
+
stop_path: paths.stopPath,
|
|
2536
|
+
receipt_path: receiptPath,
|
|
2537
|
+
}, [
|
|
2538
|
+
`Stop requested for ${name}.`,
|
|
2539
|
+
`Receipt: ${path.relative(process.cwd(), receiptPath)}`,
|
|
2540
|
+
], asJson);
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
const ticksFlag = readNumberFlag(args, '--ticks', null);
|
|
2545
|
+
const minutes = readNumberFlag(args, '--minutes', null);
|
|
2546
|
+
const durationSeconds = readNumberFlag(args, '--duration-seconds', readNumberFlag(args, '--seconds', null));
|
|
2547
|
+
const intervalSeconds = readNumberFlag(args, '--interval', readNumberFlag(args, '--interval-seconds', 60));
|
|
2548
|
+
const intervalMs = Math.max(0, Math.floor(Number(intervalSeconds == null ? 60 : intervalSeconds) * 1000));
|
|
2549
|
+
const durationMs = Math.max(0, Math.floor(durationSeconds != null ? Number(durationSeconds) * 1000 : Number(minutes == null ? 10 : minutes) * 60 * 1000));
|
|
2550
|
+
const ticks = ticksFlag != null
|
|
2551
|
+
? Math.max(1, Math.floor(Number(ticksFlag)))
|
|
2552
|
+
: intervalMs > 0
|
|
2553
|
+
? Math.max(1, Math.floor(durationMs / intervalMs))
|
|
2554
|
+
: 1;
|
|
2555
|
+
const runId = `member-loop-${name}-${fileSafeStamp()}`;
|
|
2556
|
+
const startedAt = stampIso();
|
|
2557
|
+
const ttlSeconds = readNumberFlag(args, '--lease-ttl-seconds', null);
|
|
2558
|
+
const ttlMs = Math.max(
|
|
2559
|
+
30000,
|
|
2560
|
+
Math.floor(Number(ttlSeconds == null ? 0 : ttlSeconds) * 1000),
|
|
2561
|
+
durationMs + 60000,
|
|
2562
|
+
intervalMs + 60000,
|
|
2563
|
+
);
|
|
2564
|
+
|
|
2565
|
+
if (execute && !confirmed) {
|
|
2566
|
+
const receiptPath = writeMemberLoopReceipt(name, {
|
|
2567
|
+
ok: false,
|
|
2568
|
+
action: 'loop',
|
|
2569
|
+
member: name,
|
|
2570
|
+
status: 'blocked',
|
|
2571
|
+
reason: 'execute_requires_confirm_autonomy_policy',
|
|
2572
|
+
mode: 'execute',
|
|
2573
|
+
started_at: startedAt,
|
|
2574
|
+
finished_at: stampIso(),
|
|
2575
|
+
});
|
|
2576
|
+
const payload = {
|
|
2577
|
+
ok: false,
|
|
2578
|
+
action: 'loop',
|
|
2579
|
+
member: name,
|
|
2580
|
+
status: 'blocked',
|
|
2581
|
+
reason: 'execute_requires_confirm_autonomy_policy',
|
|
2582
|
+
receipt_path: receiptPath,
|
|
2583
|
+
};
|
|
2584
|
+
writeJsonFile(paths.latestPath, payload);
|
|
2585
|
+
printJsonOrText(payload, [
|
|
2586
|
+
`Loop blocked for ${name}: execute requires --confirm-autonomy-policy.`,
|
|
2587
|
+
`Receipt: ${path.relative(process.cwd(), receiptPath)}`,
|
|
2588
|
+
], asJson);
|
|
2589
|
+
process.exitCode = 1;
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
const lease = acquireMemberLoopLease(name, { runId, ttlMs });
|
|
2594
|
+
if (!lease.acquired) {
|
|
2595
|
+
const receiptPath = writeMemberLoopReceipt(name, {
|
|
2596
|
+
ok: true,
|
|
2597
|
+
action: 'loop',
|
|
2598
|
+
member: name,
|
|
2599
|
+
status: 'skipped',
|
|
2600
|
+
reason: 'loop_already_active',
|
|
2601
|
+
mode: execute ? 'execute' : 'dry_run',
|
|
2602
|
+
active_lease: lease.lease || null,
|
|
2603
|
+
started_at: startedAt,
|
|
2604
|
+
finished_at: stampIso(),
|
|
2605
|
+
});
|
|
2606
|
+
const payload = {
|
|
2607
|
+
ok: true,
|
|
2608
|
+
action: 'loop',
|
|
2609
|
+
member: name,
|
|
2610
|
+
status: 'skipped',
|
|
2611
|
+
reason: 'loop_already_active',
|
|
2612
|
+
ticks: 0,
|
|
2613
|
+
receipt_path: receiptPath,
|
|
2614
|
+
active_lease: lease.lease || null,
|
|
2615
|
+
};
|
|
2616
|
+
writeJsonFile(paths.latestPath, payload);
|
|
2617
|
+
printJsonOrText(payload, [
|
|
2618
|
+
`Loop skipped for ${name}: another loop is active.`,
|
|
2619
|
+
`Receipt: ${path.relative(process.cwd(), receiptPath)}`,
|
|
2620
|
+
], asJson);
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
fs.rmSync(paths.stopPath, { force: true });
|
|
2625
|
+
const tickLogPath = path.join(process.cwd(), 'atris', 'runs', `${runId}.jsonl`);
|
|
2626
|
+
fs.mkdirSync(path.dirname(tickLogPath), { recursive: true });
|
|
2627
|
+
const tickResults = [];
|
|
2628
|
+
const decisions = {};
|
|
2629
|
+
let stopped = false;
|
|
2630
|
+
let failed = false;
|
|
2631
|
+
|
|
2632
|
+
try {
|
|
2633
|
+
for (let index = 0; index < ticks; index += 1) {
|
|
2634
|
+
if (fs.existsSync(paths.stopPath)) {
|
|
2635
|
+
stopped = true;
|
|
2636
|
+
break;
|
|
2637
|
+
}
|
|
2638
|
+
refreshMemberLoopLease(paths, lease.lease, ttlMs);
|
|
2639
|
+
const tickStartedAt = stampIso();
|
|
2640
|
+
try {
|
|
2641
|
+
const wake = runMemberWake(name, { execute, confirmed, force });
|
|
2642
|
+
const key = `${wake.decision}:${wake.reason}`;
|
|
2643
|
+
decisions[key] = (decisions[key] || 0) + 1;
|
|
2644
|
+
const tick = {
|
|
2645
|
+
tick: index + 1,
|
|
2646
|
+
started_at: tickStartedAt,
|
|
2647
|
+
finished_at: stampIso(),
|
|
2648
|
+
ok: true,
|
|
2649
|
+
decision: wake.decision,
|
|
2650
|
+
reason: wake.reason,
|
|
2651
|
+
executed: wake.executed,
|
|
2652
|
+
needs_user: wake.needs_user,
|
|
2653
|
+
next_command: wake.next_command,
|
|
2654
|
+
has_mission: wake.checks?.has_mission === true,
|
|
2655
|
+
has_goal: wake.checks?.has_goal === true,
|
|
2656
|
+
has_steering: wake.checks?.has_steering === true,
|
|
2657
|
+
current_experiment: wake.current_experiment?.id || null,
|
|
2658
|
+
receipt_path: wake.receipt_path,
|
|
2659
|
+
};
|
|
2660
|
+
tickResults.push(tick);
|
|
2661
|
+
fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
2662
|
+
} catch (error) {
|
|
2663
|
+
failed = true;
|
|
2664
|
+
const tick = {
|
|
2665
|
+
tick: index + 1,
|
|
2666
|
+
started_at: tickStartedAt,
|
|
2667
|
+
finished_at: stampIso(),
|
|
2668
|
+
ok: false,
|
|
2669
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2670
|
+
};
|
|
2671
|
+
tickResults.push(tick);
|
|
2672
|
+
fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
2673
|
+
break;
|
|
2674
|
+
}
|
|
2675
|
+
if (index < ticks - 1 && intervalMs > 0) sleepSync(intervalMs);
|
|
2676
|
+
}
|
|
2677
|
+
} finally {
|
|
2678
|
+
releaseMemberLoopLease(paths, lease.lease);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
const finishedAt = stampIso();
|
|
2682
|
+
const summary = {
|
|
2683
|
+
ok: !failed,
|
|
2684
|
+
action: 'loop',
|
|
2685
|
+
schema: 'atris.member_loop.v1',
|
|
2686
|
+
member: name,
|
|
2687
|
+
status: failed ? 'failed' : stopped ? 'stopped' : 'completed',
|
|
2688
|
+
mode: execute ? 'execute' : 'dry_run',
|
|
2689
|
+
run_id: runId,
|
|
2690
|
+
ticks_requested: ticks,
|
|
2691
|
+
ticks: tickResults.length,
|
|
2692
|
+
interval_ms: intervalMs,
|
|
2693
|
+
duration_ms_requested: durationMs,
|
|
2694
|
+
duration_ms_actual: Date.parse(finishedAt) - Date.parse(startedAt),
|
|
2695
|
+
decisions,
|
|
2696
|
+
has_mission_all_ticks: tickResults.length > 0 && tickResults.every((tick) => tick.has_mission === true),
|
|
2697
|
+
has_goal_all_ticks: tickResults.length > 0 && tickResults.every((tick) => tick.has_goal === true),
|
|
2698
|
+
has_steering_all_ticks: tickResults.length > 0 && tickResults.every((tick) => tick.has_steering === true),
|
|
2699
|
+
recovered_stale_lease: lease.recovered_stale,
|
|
2700
|
+
stale_lease: lease.stale_lease || null,
|
|
2701
|
+
started_at: startedAt,
|
|
2702
|
+
finished_at: finishedAt,
|
|
2703
|
+
log_path: tickLogPath,
|
|
2704
|
+
lock_path: paths.lockPath,
|
|
2705
|
+
latest_path: paths.latestPath,
|
|
2706
|
+
tick_receipts: tickResults.map((tick) => tick.receipt_path).filter(Boolean),
|
|
2707
|
+
};
|
|
2708
|
+
const receiptPath = writeMemberLoopReceipt(name, summary);
|
|
2709
|
+
const payload = { ...summary, receipt_path: receiptPath };
|
|
2710
|
+
writeJsonFile(paths.latestPath, payload);
|
|
2711
|
+
printJsonOrText(payload, [
|
|
2712
|
+
`Loop: ${name}`,
|
|
2713
|
+
`Status: ${payload.status}`,
|
|
2714
|
+
`Ticks: ${payload.ticks}/${payload.ticks_requested}`,
|
|
2715
|
+
`Decisions: ${Object.entries(decisions).map(([key, count]) => `${key} x${count}`).join(', ') || 'none'}`,
|
|
2716
|
+
`Receipt: ${path.relative(process.cwd(), receiptPath)}`,
|
|
2717
|
+
], asJson);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
function memberTick(name, ...args) {
|
|
2721
|
+
const paths = requireMemberDir(name);
|
|
2722
|
+
const asJson = hasFlag(args, '--json');
|
|
2723
|
+
const force = hasFlag(args, '--force');
|
|
2724
|
+
const goalId = readFlag(args, '--goal', '');
|
|
2725
|
+
const state = loadMemberGoals(name, paths);
|
|
2726
|
+
const goal = activeGoal(state, goalId);
|
|
2727
|
+
if (!goal) {
|
|
2728
|
+
console.error(`No active goal for ${name}. Run: atris member goal ${name} "..."`);
|
|
2729
|
+
process.exit(1);
|
|
2730
|
+
}
|
|
2731
|
+
goal.experiments = Array.isArray(goal.experiments) ? goal.experiments : [];
|
|
2732
|
+
const blocked = goal.experiments.find((item) => item.status === 'blocked') || null;
|
|
2733
|
+
if (blocked && !force) {
|
|
2734
|
+
goal.history = Array.isArray(goal.history) ? goal.history : [];
|
|
2735
|
+
goal.history.push({ at: stampIso(), event: 'tick_paused_blocked', experiment_id: blocked.id });
|
|
2736
|
+
writeMemberGoals(paths, state);
|
|
2737
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, 'Member tick paused blocked', {
|
|
2738
|
+
goal: goal.title,
|
|
2739
|
+
experiment: blocked.title,
|
|
2740
|
+
ask: blocked.block?.ask || 'Needs operator input.',
|
|
2741
|
+
orchestrator: blocked.block?.orchestrator || '',
|
|
2742
|
+
});
|
|
2743
|
+
printJsonOrText(
|
|
2744
|
+
{ ok: true, action: 'blocked', member: name, goal_id: goal.id, experiment: blocked, needs_user: true, ask: blocked.block?.ask || 'Needs operator input.', goals_path: paths.goalsJson, log_path: logPath },
|
|
2745
|
+
[
|
|
2746
|
+
`Blocked for ${name}: ${blocked.title}`,
|
|
2747
|
+
`Ask: ${blocked.block?.ask || 'Needs operator input.'}`,
|
|
2748
|
+
`Next: atris member review ${name} ${blocked.id} --discard --proof "..."`,
|
|
2749
|
+
],
|
|
2750
|
+
asJson,
|
|
2751
|
+
);
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
let experiment = goal.experiments.find((item) => item.status === 'proposed' || item.status === 'running') || null;
|
|
2755
|
+
const reused = Boolean(experiment && !force);
|
|
2756
|
+
if (!experiment || force) {
|
|
2757
|
+
experiment = proposalForGoal(goal);
|
|
2758
|
+
goal.experiments.push(experiment);
|
|
2759
|
+
}
|
|
2760
|
+
goal.history = Array.isArray(goal.history) ? goal.history : [];
|
|
2761
|
+
goal.history.push({ at: stampIso(), event: reused ? 'tick_reused_proposal' : 'tick_proposed_experiment', experiment_id: experiment.id });
|
|
2762
|
+
writeMemberGoals(paths, state);
|
|
2763
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, reused ? 'Member tick reused proposal' : 'Member tick proposed experiment', {
|
|
2764
|
+
goal: goal.title,
|
|
2765
|
+
experiment: experiment.title,
|
|
2766
|
+
proof_target: experiment.proof_target,
|
|
2767
|
+
next_step: experiment.next_step || '',
|
|
2768
|
+
verifier: experiment.verifier || '',
|
|
2769
|
+
});
|
|
2770
|
+
printJsonOrText(
|
|
2771
|
+
{ ok: true, action: 'tick', member: name, goal_id: goal.id, experiment, reused, goals_path: paths.goalsJson, log_path: logPath },
|
|
2772
|
+
[
|
|
2773
|
+
`${reused ? 'Reusing' : 'Proposed'} experiment for ${name}: ${experiment.title}`,
|
|
2774
|
+
`Proof target: ${experiment.proof_target}`,
|
|
2775
|
+
`Stop rule: ${experiment.stop_rule}`,
|
|
2776
|
+
],
|
|
2777
|
+
asJson,
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
function memberReview(name, experimentId, ...args) {
|
|
2782
|
+
const paths = requireMemberDir(name);
|
|
2783
|
+
const asJson = hasFlag(args, '--json');
|
|
2784
|
+
const accepted = hasFlag(args, '--accept');
|
|
2785
|
+
const discarded = hasFlag(args, '--discard');
|
|
2786
|
+
if (!experimentId || accepted === discarded) {
|
|
2787
|
+
console.error('Usage: atris member review <name> <experiment-id> (--accept|--discard) --proof "..." [--lesson "..."] [--next "..."]');
|
|
2788
|
+
process.exit(1);
|
|
2789
|
+
}
|
|
2790
|
+
const proof = readFlag(args, '--proof', '');
|
|
2791
|
+
if (!proof) {
|
|
2792
|
+
console.error('Refusing review without --proof.');
|
|
2793
|
+
process.exit(1);
|
|
2794
|
+
}
|
|
2795
|
+
const value = readNumberFlag(args, '--value', null);
|
|
2796
|
+
if (value != null && (!Number.isInteger(value) || value < 1 || value > 5)) {
|
|
2797
|
+
console.error('Value must be an integer from 1 to 5.');
|
|
2798
|
+
process.exit(1);
|
|
2799
|
+
}
|
|
2800
|
+
const lesson = readFlag(args, '--lesson', '');
|
|
2801
|
+
const nextTitle = readFlag(args, '--next', '');
|
|
2802
|
+
const state = loadMemberGoals(name, paths);
|
|
2803
|
+
const { goal: foundGoal, experiment } = findExperiment(state, experimentId);
|
|
2804
|
+
if (!foundGoal || !experiment) {
|
|
2805
|
+
console.error(`Experiment "${experimentId}" not found for ${name}.`);
|
|
2806
|
+
process.exit(1);
|
|
2807
|
+
}
|
|
2808
|
+
experiment.status = accepted ? 'accepted' : 'discarded';
|
|
2809
|
+
experiment.reviewed_at = stampIso();
|
|
2810
|
+
experiment.proof = proof;
|
|
2811
|
+
if (value != null) experiment.value = value;
|
|
2812
|
+
if (lesson) experiment.lesson = lesson;
|
|
2813
|
+
let nextExperiment = null;
|
|
2814
|
+
if (accepted && nextTitle) {
|
|
2815
|
+
nextExperiment = {
|
|
2816
|
+
id: makeExperimentId(foundGoal.id, nextTitle),
|
|
2817
|
+
title: nextTitle,
|
|
2818
|
+
status: 'proposed',
|
|
2819
|
+
proof_target: (foundGoal.acceptance || [])[0] || 'Return proof, risk, and next move.',
|
|
2820
|
+
stop_rule: 'Stop if proof is missing, risk is unclear, or the next move would require new authority.',
|
|
2821
|
+
created_at: stampIso(),
|
|
2822
|
+
source: `review:${experiment.id}`,
|
|
2823
|
+
};
|
|
2824
|
+
foundGoal.experiments.push(nextExperiment);
|
|
2825
|
+
}
|
|
2826
|
+
foundGoal.history = Array.isArray(foundGoal.history) ? foundGoal.history : [];
|
|
2827
|
+
foundGoal.history.push({
|
|
2828
|
+
at: stampIso(),
|
|
2829
|
+
event: accepted ? 'experiment_accepted' : 'experiment_discarded',
|
|
2830
|
+
experiment_id: experiment.id,
|
|
2831
|
+
next_experiment_id: nextExperiment?.id,
|
|
2832
|
+
value,
|
|
2833
|
+
});
|
|
2834
|
+
writeMemberGoals(paths, state);
|
|
2835
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, accepted ? 'Member experiment accepted' : 'Member experiment discarded', {
|
|
2836
|
+
goal: foundGoal.title,
|
|
2837
|
+
experiment: experiment.title,
|
|
2838
|
+
proof,
|
|
2839
|
+
lesson,
|
|
2840
|
+
value: value == null ? '' : `${value}/5`,
|
|
2841
|
+
next: nextExperiment?.title || '',
|
|
2842
|
+
});
|
|
2843
|
+
printJsonOrText(
|
|
2844
|
+
{ ok: true, action: 'review', member: name, goal_id: foundGoal.id, experiment, outcome: experiment.status, value, next_experiment: nextExperiment, goals_path: paths.goalsJson, log_path: logPath },
|
|
2845
|
+
[
|
|
2846
|
+
`${accepted ? 'Accepted' : 'Discarded'} experiment for ${name}: ${experiment.title}`,
|
|
2847
|
+
`Proof: ${proof}`,
|
|
2848
|
+
...(value == null ? [] : [`Value: ${value}/5`]),
|
|
2849
|
+
...(nextExperiment ? [`Next proposed: ${nextExperiment.title}`] : []),
|
|
2850
|
+
],
|
|
2851
|
+
asJson,
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
function memberBlock(name, experimentId, ...args) {
|
|
2856
|
+
const paths = requireMemberDir(name);
|
|
2857
|
+
const asJson = hasFlag(args, '--json');
|
|
2858
|
+
const reason = readFlag(args, '--reason', '');
|
|
2859
|
+
const ask = readFlag(args, '--ask', '');
|
|
2860
|
+
const orchestrator = readFlag(args, '--orchestrator', '');
|
|
2861
|
+
if (!experimentId || !reason || !ask) {
|
|
2862
|
+
console.error('Usage: atris member block <name> <experiment-id> --reason "..." --ask "..." [--orchestrator name]');
|
|
2863
|
+
process.exit(1);
|
|
2864
|
+
}
|
|
2865
|
+
const state = loadMemberGoals(name, paths);
|
|
2866
|
+
const { goal, experiment } = findExperiment(state, experimentId);
|
|
2867
|
+
if (!goal || !experiment) {
|
|
2868
|
+
console.error(`Experiment "${experimentId}" not found for ${name}.`);
|
|
2869
|
+
process.exit(1);
|
|
2870
|
+
}
|
|
2871
|
+
experiment.status = 'blocked';
|
|
2872
|
+
experiment.blocked_at = stampIso();
|
|
2873
|
+
experiment.block = { reason, ask, orchestrator: orchestrator || null };
|
|
2874
|
+
goal.history = Array.isArray(goal.history) ? goal.history : [];
|
|
2875
|
+
goal.history.push({ at: stampIso(), event: 'experiment_blocked', experiment_id: experiment.id, ask, orchestrator: orchestrator || null });
|
|
2876
|
+
writeMemberGoals(paths, state);
|
|
2877
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, 'Member experiment blocked', {
|
|
2878
|
+
goal: goal.title,
|
|
2879
|
+
experiment: experiment.title,
|
|
2880
|
+
reason,
|
|
2881
|
+
ask,
|
|
2882
|
+
orchestrator,
|
|
2883
|
+
});
|
|
2884
|
+
printJsonOrText(
|
|
2885
|
+
{ ok: true, action: 'blocked', member: name, goal_id: goal.id, experiment, needs_user: true, ask, orchestrator: orchestrator || null, goals_path: paths.goalsJson, log_path: logPath },
|
|
2886
|
+
[
|
|
2887
|
+
`Blocked ${name}: ${experiment.title}`,
|
|
2888
|
+
`Reason: ${reason}`,
|
|
2889
|
+
`Ask: ${ask}`,
|
|
2890
|
+
...(orchestrator ? [`Orchestrator: ${orchestrator}`] : []),
|
|
2891
|
+
],
|
|
2892
|
+
asJson,
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
function memberStatus(name, ...args) {
|
|
2897
|
+
const paths = requireMemberDir(name);
|
|
2898
|
+
const asJson = hasFlag(args, '--json');
|
|
2899
|
+
const state = loadMemberGoals(name, paths);
|
|
2900
|
+
const goal = activeGoal(state);
|
|
2901
|
+
const current = memberOpenExperiment(state);
|
|
2902
|
+
const lastReviewed = memberLastReviewedExperiment(state);
|
|
2903
|
+
const value = memberValueSummary(state);
|
|
2904
|
+
const logs = recentLogLines(paths.memberDir);
|
|
2905
|
+
const needsUser = current?.status === 'blocked';
|
|
2906
|
+
const stateLabel = !goal
|
|
2907
|
+
? 'no_goal'
|
|
2908
|
+
: needsUser
|
|
2909
|
+
? 'needs_user'
|
|
2910
|
+
: current
|
|
2911
|
+
? current.status
|
|
2912
|
+
: 'ready';
|
|
2913
|
+
const ask = needsUser ? (current.block?.ask || 'Needs operator input.') : null;
|
|
2914
|
+
const nextCommand = !goal
|
|
2915
|
+
? `atris member goal ${name} "..."`
|
|
2916
|
+
: needsUser
|
|
2917
|
+
? `atris member review ${name} ${current.id} --discard --proof "..."`
|
|
2918
|
+
: current
|
|
2919
|
+
? `atris member review ${name} ${current.id} --accept --proof "..." --value 4`
|
|
2920
|
+
: `atris member tick ${name}`;
|
|
2921
|
+
const payload = {
|
|
2922
|
+
ok: true,
|
|
2923
|
+
action: 'status',
|
|
2924
|
+
member: name,
|
|
2925
|
+
state: stateLabel,
|
|
2926
|
+
needs_user: needsUser,
|
|
2927
|
+
ask,
|
|
2928
|
+
active_goal: goal || null,
|
|
2929
|
+
current_experiment: current || null,
|
|
2930
|
+
last_reviewed: lastReviewed || null,
|
|
2931
|
+
value,
|
|
2932
|
+
next_command: nextCommand,
|
|
2933
|
+
recent_log: logs,
|
|
2934
|
+
goals_path: paths.goalsJson,
|
|
2935
|
+
goals_md_path: paths.goalsMd,
|
|
2936
|
+
};
|
|
2937
|
+
printJsonOrText(
|
|
2938
|
+
payload,
|
|
2939
|
+
[
|
|
2940
|
+
`Member: ${name}`,
|
|
2941
|
+
`State: ${stateLabel}`,
|
|
2942
|
+
`Goal: ${goal?.title || 'No goal yet'}`,
|
|
2943
|
+
`Current: ${current ? `${current.status} - ${current.title}` : 'No open experiment'}`,
|
|
2944
|
+
...(ask ? [`Ask: ${ask}`] : []),
|
|
2945
|
+
`Value: ${value.line}`,
|
|
2946
|
+
`Next: ${nextCommand}`,
|
|
2947
|
+
...(logs.length ? ['Recent log:', ...logs.map((line) => ` ${line}`)] : []),
|
|
2948
|
+
],
|
|
2949
|
+
asJson,
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
|
|
604
2953
|
// --- Command Dispatcher ---
|
|
605
2954
|
|
|
606
2955
|
function memberCommand(subcommand, ...args) {
|
|
2956
|
+
// Subcommands that take a member name as args[0] otherwise treat `--help` as
|
|
2957
|
+
// a name and error with "Member '--help' not found". `create`/`new` handle
|
|
2958
|
+
// help themselves (with subcommand-specific usage) — leave those alone.
|
|
2959
|
+
const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new']);
|
|
2960
|
+
if (!HELP_AWARE_SUBCOMMANDS.has(subcommand) && (args[0] === '-h' || args[0] === '--help')) {
|
|
2961
|
+
subcommand = undefined;
|
|
2962
|
+
}
|
|
607
2963
|
switch (subcommand) {
|
|
608
2964
|
case 'list':
|
|
609
2965
|
case 'ls':
|
|
@@ -619,6 +2975,32 @@ function memberCommand(subcommand, ...args) {
|
|
|
619
2975
|
return memberPush(args[0]);
|
|
620
2976
|
case 'pull':
|
|
621
2977
|
return memberPull(args[0]);
|
|
2978
|
+
case 'goal':
|
|
2979
|
+
return memberGoal(args[0], ...args.slice(1));
|
|
2980
|
+
case 'goal-from-mission':
|
|
2981
|
+
case 'mission-goal':
|
|
2982
|
+
return memberGoalFromMission(args[0], ...args.slice(1));
|
|
2983
|
+
case 'goal-from-score':
|
|
2984
|
+
case 'score-goal':
|
|
2985
|
+
return memberGoalFromScore(args[0], ...args.slice(1));
|
|
2986
|
+
case 'tick':
|
|
2987
|
+
return memberTick(args[0], ...args.slice(1));
|
|
2988
|
+
case 'wake':
|
|
2989
|
+
return memberWake(args[0], ...args.slice(1));
|
|
2990
|
+
case 'run':
|
|
2991
|
+
return memberRun(args[0], ...args.slice(1));
|
|
2992
|
+
case 'loop':
|
|
2993
|
+
return memberLoop(args[0], ...args.slice(1));
|
|
2994
|
+
case 'review':
|
|
2995
|
+
return memberReview(args[0], args[1], ...args.slice(2));
|
|
2996
|
+
case 'block':
|
|
2997
|
+
return memberBlock(args[0], args[1], ...args.slice(2));
|
|
2998
|
+
case 'status':
|
|
2999
|
+
return memberStatus(args[0], ...args.slice(1));
|
|
3000
|
+
case 'archive':
|
|
3001
|
+
return memberArchive(args[0]);
|
|
3002
|
+
case 'purge-archived':
|
|
3003
|
+
return memberPurgeArchived(...args);
|
|
622
3004
|
default:
|
|
623
3005
|
console.log('');
|
|
624
3006
|
console.log('Usage: atris member <subcommand> [name]');
|
|
@@ -630,6 +3012,18 @@ function memberCommand(subcommand, ...args) {
|
|
|
630
3012
|
console.log(' upgrade <name> Convert flat file (name.md) to directory format');
|
|
631
3013
|
console.log(' push <name> Push a local team member to the cloud');
|
|
632
3014
|
console.log(' pull <name|id> Pull a cloud agent as a local team member');
|
|
3015
|
+
console.log(' goal <name> "..." Create/update a member long-term goal');
|
|
3016
|
+
console.log(' goal-from-mission <name> Create/reuse a goal from MISSION.md and now.md');
|
|
3017
|
+
console.log(' goal-from-score <name> Create/reuse an active goal from Team score evidence');
|
|
3018
|
+
console.log(' wake <name> Read Mission state and decide tick/wait/ask/stop');
|
|
3019
|
+
console.log(" run <name> Run the member's active Mission Runtime");
|
|
3020
|
+
console.log(' loop <name> Repeat wake on a bounded cadence with a no-overlap lease');
|
|
3021
|
+
console.log(' tick <name> Propose the next bounded experiment');
|
|
3022
|
+
console.log(' review <name> <id> Accept/discard an experiment with proof');
|
|
3023
|
+
console.log(' block <name> <id> Mark an experiment blocked with a human/orchestrator ask');
|
|
3024
|
+
console.log(' status <name> Show goal, open experiment, value, ask, and recent log');
|
|
3025
|
+
console.log(' archive <name> Move a member to atris/team/_archived/');
|
|
3026
|
+
console.log(' purge-archived Delete archived members older than --days=60 with confirmation');
|
|
633
3027
|
console.log('');
|
|
634
3028
|
console.log('Create flags:');
|
|
635
3029
|
console.log(' --role="Title" Set the member role');
|
|
@@ -643,6 +3037,19 @@ function memberCommand(subcommand, ...args) {
|
|
|
643
3037
|
console.log(' atris member upgrade executor');
|
|
644
3038
|
console.log(' atris member push navigator');
|
|
645
3039
|
console.log(' atris member pull navigator (reads agent-id from local MEMBER.md)');
|
|
3040
|
+
console.log(' atris member goal growth "Recover more customer revenue" --acceptance "one proof-backed action"');
|
|
3041
|
+
console.log(' atris member goal-from-mission growth --json');
|
|
3042
|
+
console.log(' atris member goal-from-score growth --score-json team-score.json --json');
|
|
3043
|
+
console.log(' atris member wake growth --json');
|
|
3044
|
+
console.log(' atris member run growth --max-ticks 1 --max-wall 900 --json');
|
|
3045
|
+
console.log(' atris member wake growth --execute --confirm-autonomy-policy');
|
|
3046
|
+
console.log(' atris member loop growth --minutes 10 --interval 60 --json');
|
|
3047
|
+
console.log(' atris member loop growth --ticks 2 --interval 0 --json');
|
|
3048
|
+
console.log(' atris member tick growth --json');
|
|
3049
|
+
console.log(' atris member status growth');
|
|
3050
|
+
console.log(' atris member review growth exp_123 --accept --proof "validated" --value 4');
|
|
3051
|
+
console.log(' atris member archive old-member');
|
|
3052
|
+
console.log(' atris member purge-archived --days=60 --confirm "delete archived members"');
|
|
646
3053
|
console.log('');
|
|
647
3054
|
}
|
|
648
3055
|
}
|