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.
Files changed (93) hide show
  1. package/AGENTS.md +84 -8
  2. package/README.md +5 -1
  3. package/atris/AGENTS.md +46 -1
  4. package/atris/CLAUDE.md +36 -1
  5. package/atris/GEMINI.md +14 -1
  6. package/atris/atris.md +12 -1
  7. package/atris/atrisDev.md +3 -2
  8. package/atris/context/README.md +11 -0
  9. package/atris/features/company-brain-sync/validate.md +5 -5
  10. package/atris/learnings.jsonl +1 -0
  11. package/atris/policies/atris-design.md +2 -0
  12. package/atris/skills/aeo/SKILL.md +2 -2
  13. package/atris/skills/atris/SKILL.md +15 -62
  14. package/atris/skills/design/SKILL.md +2 -0
  15. package/atris/skills/imessage/SKILL.md +19 -2
  16. package/atris/skills/loop/SKILL.md +6 -5
  17. package/atris/skills/magic-inbox/SKILL.md +1 -1
  18. package/atris/team/_template/MEMBER.md +23 -1
  19. package/atris/team/brainstormer/START_HERE.md +6 -0
  20. package/atris/team/executor/MEMBER.md +13 -0
  21. package/atris/team/executor/START_HERE.md +6 -0
  22. package/atris/team/launcher/START_HERE.md +6 -0
  23. package/atris/team/mission-lead/MEMBER.md +39 -0
  24. package/atris/team/mission-lead/MISSION.md +33 -0
  25. package/atris/team/mission-lead/START_HERE.md +6 -0
  26. package/atris/team/navigator/MEMBER.md +11 -0
  27. package/atris/team/navigator/START_HERE.md +6 -0
  28. package/atris/team/opus-overnight/MEMBER.md +39 -0
  29. package/atris/team/opus-overnight/MISSION.md +61 -0
  30. package/atris/team/opus-overnight/START_HERE.md +6 -0
  31. package/atris/team/opus-overnight/STEERING.md +35 -0
  32. package/atris/team/researcher/START_HERE.md +6 -0
  33. package/atris/team/validator/MEMBER.md +26 -6
  34. package/atris/team/validator/START_HERE.md +6 -0
  35. package/atris/wiki/concepts/agent-activation-contract.md +79 -0
  36. package/atris/wiki/concepts/workspace-initialization-contract.md +73 -0
  37. package/atris/wiki/index.md +27 -0
  38. package/atris/wiki/sources/atris-labs-2026-05-10.txt +17 -0
  39. package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +15 -0
  40. package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +10 -0
  41. package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +12 -0
  42. package/atris.md +49 -13
  43. package/bin/atris.js +660 -22
  44. package/commands/activate.js +12 -3
  45. package/commands/aeo.js +1 -1
  46. package/commands/align.js +10 -10
  47. package/commands/analytics.js +9 -4
  48. package/commands/app.js +2 -0
  49. package/commands/apps.js +276 -0
  50. package/commands/auth.js +1 -1
  51. package/commands/autopilot.js +74 -5
  52. package/commands/brain.js +536 -61
  53. package/commands/brainstorm.js +12 -12
  54. package/commands/business-sync.js +142 -24
  55. package/commands/clean.js +9 -6
  56. package/commands/codex-goal.js +311 -0
  57. package/commands/errors.js +11 -1
  58. package/commands/feedback.js +55 -17
  59. package/commands/fork.js +2 -2
  60. package/commands/gm.js +376 -0
  61. package/commands/init.js +80 -3
  62. package/commands/integrations.js +524 -0
  63. package/commands/learn.js +25 -16
  64. package/commands/lesson.js +41 -0
  65. package/commands/lifecycle.js +2 -2
  66. package/commands/member.js +2416 -9
  67. package/commands/mission.js +1776 -0
  68. package/commands/now.js +48 -7
  69. package/commands/play.js +425 -0
  70. package/commands/publish.js +2 -1
  71. package/commands/pull.js +72 -29
  72. package/commands/push.js +199 -17
  73. package/commands/review.js +51 -13
  74. package/commands/skill.js +2 -2
  75. package/commands/soul.js +19 -13
  76. package/commands/status.js +6 -1
  77. package/commands/sync.js +5 -4
  78. package/commands/task.js +1041 -147
  79. package/commands/terminal.js +5 -5
  80. package/commands/verify.js +7 -5
  81. package/commands/visualize.js +7 -0
  82. package/commands/wiki.js +53 -16
  83. package/commands/workflow.js +298 -54
  84. package/commands/workspace-clean.js +1 -1
  85. package/commands/worktree.js +468 -0
  86. package/commands/xp.js +1608 -0
  87. package/lib/manifest.js +34 -4
  88. package/lib/scorecard.js +3 -2
  89. package/lib/task-db.js +408 -27
  90. package/lib/todo-fallback.js +28 -2
  91. package/lib/todo.js +5 -3
  92. package/package.json +23 -2
  93. package/utils/update-check.js +51 -1
@@ -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(s) found.`);
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 (!name) {
193
- console.error('Usage: atris member create <name> [--role="Title"] [--push]');
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
- fs.mkdirSync(path.join(memberDir, 'journal'), { recursive: true });
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}/journal/`);
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
- fs.mkdirSync(path.join(memberDir, 'journal'), { recursive: true });
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/, journal/ directories`);
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
- fs.mkdirSync(path.join(memberDir, 'journal'), { recursive: true });
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 localPath = path.join(memberDir, file.path);
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
  }