atris 3.58.5 → 3.58.7

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 (42) hide show
  1. package/README.md +8 -0
  2. package/atris/policies/engineering-principles.md +129 -0
  3. package/atris/policies/genesis.md +112 -0
  4. package/atris/policies/product-design-principles.md +100 -0
  5. package/atris/skills/design/SKILL.md +3 -1
  6. package/atris/skills/engines/SKILL.md +3 -3
  7. package/atris/skills/x-search/SKILL.md +2 -2
  8. package/atris/skills/youtube/SKILL.md +44 -28
  9. package/bin/atris.js +56 -3
  10. package/commands/auth.js +58 -24
  11. package/commands/brain.js +1 -0
  12. package/commands/design.js +362 -0
  13. package/commands/doc-health.js +329 -0
  14. package/commands/drive.js +32 -0
  15. package/commands/improve.js +67 -1
  16. package/commands/land.js +144 -4
  17. package/commands/learn.js +211 -40
  18. package/commands/member.js +65 -11
  19. package/commands/mission.js +37 -7
  20. package/commands/pulse.js +38 -0
  21. package/commands/rsi.js +156 -0
  22. package/commands/task.js +41 -1
  23. package/commands/workflow.js +15 -14
  24. package/commands/x-search.js +9 -10
  25. package/commands/youtube.js +518 -107
  26. package/lib/apply-gate.js +22 -4
  27. package/lib/daily-log.js +88 -0
  28. package/lib/design-api.js +130 -0
  29. package/lib/engine-ask.js +1 -1
  30. package/lib/first-minute.js +1 -6
  31. package/lib/known-commands.js +3 -3
  32. package/lib/member-context.js +42 -0
  33. package/lib/rsi-record.js +335 -0
  34. package/lib/state-detection.js +8 -8
  35. package/lib/task-db.js +71 -51
  36. package/lib/task-list-keeper.js +192 -0
  37. package/lib/todo-fallback.js +9 -3
  38. package/lib/todo.js +22 -10
  39. package/mcp/atris-mcp/index.mjs +174 -0
  40. package/package.json +8 -3
  41. package/scripts/det/ytnotes +122 -10
  42. package/utils/auth.js +109 -13
package/commands/learn.js CHANGED
@@ -12,6 +12,17 @@ const {
12
12
  getStats,
13
13
  exportMarkdown,
14
14
  } = require('../lib/learnings');
15
+ const applyGate = require('../lib/apply-gate');
16
+ const {
17
+ fileTeachExperiment,
18
+ extractTeachNumbers,
19
+ extractTeachMechanisms,
20
+ isThinTeachLesson,
21
+ printLearnerCheckGate,
22
+ proveSavedLearnerBaseline,
23
+ } = require('./youtube');
24
+
25
+ const KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the fixture contains the check tokens.';
15
26
 
16
27
  function showRecent(limit = 20) {
17
28
  const learnings = loadLearnings()
@@ -149,7 +160,52 @@ function showPrune() {
149
160
  console.log('');
150
161
  }
151
162
 
152
- function interactiveAdd() {
163
+ function commitAddedLearning({ type, key, insight, confidence, source, files } = {}, deps = {}) {
164
+ const print = typeof deps.output === 'function' ? deps.output : (line = '') => console.log(line);
165
+ const cwd = deps.cwd || process.cwd();
166
+ const entry = addLearning({ type, key, insight, confidence, source, files });
167
+ print('');
168
+ print(` ✓ Saved: [${entry.confidence}/10] ${entry.type}/${entry.key}`);
169
+ print(` "${entry.insight}"`);
170
+ print('');
171
+ const baseline = mintRichLearn({
172
+ cwd,
173
+ key: entry.key,
174
+ insight: entry.insight,
175
+ now: deps.now,
176
+ output: print,
177
+ });
178
+ return { entry, baseline };
179
+ }
180
+
181
+ function reviewLearningType(insight) {
182
+ return /^(don't|never|avoid|watch out|careful)/i.test(String(insight || '')) ? 'pitfall' : 'pattern';
183
+ }
184
+
185
+ function reviewLearningKey(insight) {
186
+ return String(insight || '').toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).slice(0, 4).join('-');
187
+ }
188
+
189
+ function commitReviewLearning(insight, deps = {}) {
190
+ const print = typeof deps.output === 'function' ? deps.output : (line = '') => console.log(line);
191
+ const cwd = deps.cwd || process.cwd();
192
+ const text = String(insight || '').trim();
193
+ if (!text) return { entry: null, baseline: 0 };
194
+ const type = reviewLearningType(text);
195
+ const key = reviewLearningKey(text);
196
+ const entry = addLearning({ type, key, insight: text, confidence: 7, source: 'review', files: [] });
197
+ print(`✓ Saved to learnings: [7/10] ${entry.type}/${entry.key}`);
198
+ const baseline = mintRichLearn({
199
+ cwd,
200
+ key: entry.key,
201
+ insight: entry.insight,
202
+ now: deps.now,
203
+ output: print,
204
+ });
205
+ return { entry, baseline };
206
+ }
207
+
208
+ function interactiveAdd(deps = {}) {
153
209
  const rl = readline.createInterface({
154
210
  input: process.stdin,
155
211
  output: process.stdout,
@@ -214,11 +270,11 @@ function interactiveAdd() {
214
270
  return;
215
271
  }
216
272
 
217
- const entry = addLearning({ type, key, insight, confidence, source, files });
218
- console.log('');
219
- console.log(` ✓ Saved: [${entry.confidence}/10] ${entry.type}/${entry.key}`);
220
- console.log(` "${entry.insight}"`);
221
- console.log('');
273
+ commitAddedLearning({ type, key, insight, confidence, source, files }, {
274
+ cwd: deps.cwd || process.cwd(),
275
+ now: deps.now,
276
+ output: deps.output,
277
+ });
222
278
  rl.close();
223
279
  } catch (err) {
224
280
  console.log(` ✗ Error: ${err.message}`);
@@ -239,24 +295,104 @@ function printLearnLogSchema(stream = console.error) {
239
295
  for (const line of learnLogSchemaLines()) stream(` ${line}`);
240
296
  }
241
297
 
298
+ function learnExperimentSlug(key) {
299
+ return `learn-${applyGate.applySlug(key)}`;
300
+ }
301
+
302
+ function learnExperimentRel(key) {
303
+ return `atris/experiments/${learnExperimentSlug(key)}`;
304
+ }
305
+
306
+ function learnApplyRel(key) {
307
+ return applyGate.applySidecarRel('learn', applyGate.applySlug(key));
308
+ }
309
+
310
+ function learnLessonFromText(text) {
311
+ const body = String(text || '');
312
+ return {
313
+ numbers: extractTeachNumbers(body),
314
+ mechanisms: extractTeachMechanisms(body),
315
+ };
316
+ }
317
+
318
+ function saveRichLearn({ cwd, key, insight } = {}) {
319
+ const lesson = learnLessonFromText(insight);
320
+ if (isThinTeachLesson(lesson)) {
321
+ return { thin: true, packRel: null, lesson };
322
+ }
323
+ if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
324
+ const packRel = fileTeachExperiment({
325
+ cwd,
326
+ lesson,
327
+ slug: key ? learnExperimentSlug(key) : null,
328
+ applyRel: key ? learnApplyRel(key) : null,
329
+ });
330
+ return { thin: false, packRel, lesson };
331
+ }
332
+
333
+ function ensureLearnApply({ cwd, key, packRel, now, output } = {}) {
334
+ const pack = packRel || (key ? learnExperimentRel(key) : null);
335
+ const slug = pack ? path.basename(pack) : null;
336
+ return applyGate.ensureApply({
337
+ cwd,
338
+ source: key ? `learn:${key}` : 'learn',
339
+ rel: key ? learnApplyRel(key) : null,
340
+ now,
341
+ output,
342
+ incompleteMessage: slug
343
+ ? `next: atris experiments keep ${slug}`
344
+ : applyGate.ephemeralApplyMessage('learning'),
345
+ required: false,
346
+ change: pack ? `apply ${pack}` : undefined,
347
+ receipt: pack ? KEEP_RULE : undefined,
348
+ journalLine: pack ? `- [claimable] apply: ${pack}. ${KEEP_RULE}` : undefined,
349
+ });
350
+ }
351
+
352
+ function mintRichLearn({ cwd, key, insight, now, output } = {}) {
353
+ const print = typeof output === 'function' ? output : (line = '') => console.log(line);
354
+ const saved = saveRichLearn({ cwd, key, insight });
355
+ if (saved.thin) {
356
+ printLearnerCheckGate(print, saved.lesson, { includeCheck: true });
357
+ return 0;
358
+ }
359
+ ensureLearnApply({
360
+ cwd,
361
+ key,
362
+ packRel: saved.packRel,
363
+ now,
364
+ output: print,
365
+ });
366
+ return proveSavedLearnerBaseline({
367
+ cwd,
368
+ applyRel: key ? learnApplyRel(key) : null,
369
+ lesson: saved.lesson,
370
+ output: print,
371
+ });
372
+ }
373
+
242
374
  /**
243
375
  * Non-interactive log: `atris learn log '{"type":"pattern","key":"...","insight":"...","confidence":8,"source":"observed"}'`
244
376
  * For agents and scripts, no prompts, no quality gate.
245
377
  * Aliases: title→key, detail→insight.
246
378
  */
247
- function logDirect(jsonStr) {
379
+ function logDirect(jsonStr, deps = {}) {
380
+ const error = typeof deps.error === 'function' ? deps.error : (line = '') => console.error(line);
381
+ const print = typeof deps.output === 'function' ? deps.output : (line = '') => console.log(line);
382
+ const exit = typeof deps.exit === 'function' ? deps.exit : (code) => process.exit(code);
383
+ const cwd = deps.cwd || process.cwd();
248
384
  if (!jsonStr) {
249
- console.error(' ✗ Usage: atris learn log \'<json>\'');
250
- printLearnLogSchema();
251
- process.exit(1);
385
+ error(' ✗ Usage: atris learn log \'<json>\'');
386
+ printLearnLogSchema(error);
387
+ return exit(1);
252
388
  }
253
389
  let data;
254
390
  try {
255
391
  data = JSON.parse(jsonStr);
256
392
  } catch (err) {
257
- console.error(` ✗ Invalid JSON: ${err.message}`);
258
- printLearnLogSchema();
259
- process.exit(1);
393
+ error(` ✗ Invalid JSON: ${err.message}`);
394
+ printLearnLogSchema(error);
395
+ return exit(1);
260
396
  }
261
397
  try {
262
398
  const entry = addLearning({
@@ -267,24 +403,39 @@ function logDirect(jsonStr) {
267
403
  source: data.source || 'observed',
268
404
  files: data.files || [],
269
405
  });
270
- console.log(` ✓ [${entry.confidence}/10] ${entry.type}/${entry.key}`);
406
+ print(` ✓ [${entry.confidence}/10] ${entry.type}/${entry.key}`);
407
+ const baseline = mintRichLearn({
408
+ cwd,
409
+ key: entry.key,
410
+ insight: entry.insight,
411
+ now: deps.now,
412
+ output: print,
413
+ });
414
+ if (baseline !== 0) return exit(baseline);
415
+ return 0;
271
416
  } catch (err) {
272
- console.error(` ✗ ${err.message}`);
273
- printLearnLogSchema();
274
- process.exit(1);
417
+ error(` ✗ ${err.message}`);
418
+ printLearnLogSchema(error);
419
+ return exit(1);
275
420
  }
276
421
  }
277
422
 
423
+ function leftoverClaimableInsight(insight) {
424
+ return /^\[claimable\]/i.test(String(insight || '').trim());
425
+ }
426
+
278
427
  /**
279
428
  * Harvest learnings from journal Notes sections.
280
429
  * Scans recent journals for lines that look like insights.
281
430
  */
282
- function harvestFromJournals() {
283
- const atrisDir = path.join(process.cwd(), 'atris');
431
+ function harvestFromJournals(deps = {}) {
432
+ const print = typeof deps.output === 'function' ? deps.output : (line = '') => console.log(line);
433
+ const cwd = deps.cwd || process.cwd();
434
+ const atrisDir = path.join(cwd, 'atris');
284
435
  const logsDir = path.join(atrisDir, 'logs');
285
436
 
286
437
  if (!fs.existsSync(logsDir)) {
287
- console.log(' No journals found.');
438
+ print(' No journals found.');
288
439
  return;
289
440
  }
290
441
 
@@ -303,13 +454,14 @@ function harvestFromJournals() {
303
454
  // Scan last 7 journals for Notes section entries
304
455
  const candidates = [];
305
456
  for (const logPath of allLogs.slice(0, 7)) {
306
- const content = fs.readFileSync(logPath, 'utf8');
457
+ const content = fs.readFileSync(logPath, 'utf8').replace(/\r\n/g, '\n');
307
458
  const notesMatch = content.match(/## Notes\n([\s\S]*?)(?=\n## |$)/);
308
459
  if (notesMatch && notesMatch[1].trim()) {
309
460
  const lines = notesMatch[1].trim().split('\n').filter(l => l.startsWith('- '));
310
461
  for (const line of lines) {
311
462
  // Strip bullet and optional timestamp prefix
312
463
  const insight = line.replace(/^- (\d{2}:\d{2} \u2014 )?/, '').trim();
464
+ if (leftoverClaimableInsight(insight)) continue;
313
465
  if (insight.length > 10) {
314
466
  candidates.push({ insight, source: path.basename(logPath) });
315
467
  }
@@ -318,10 +470,10 @@ function harvestFromJournals() {
318
470
  }
319
471
 
320
472
  if (candidates.length === 0) {
321
- console.log('');
322
- console.log(' No harvestable notes found in recent journals.');
323
- console.log(' Add notes during "atris review" or write to ## Notes in your journal.');
324
- console.log('');
473
+ print('');
474
+ print(' No harvestable notes found in recent journals.');
475
+ print(' Add notes during "atris review" or write to ## Notes in your journal.');
476
+ print('');
325
477
  return;
326
478
  }
327
479
 
@@ -331,31 +483,38 @@ function harvestFromJournals() {
331
483
  const fresh = candidates.filter(c => !existingInsights.has(c.insight.toLowerCase()));
332
484
 
333
485
  if (fresh.length === 0) {
334
- console.log('');
335
- console.log(` Scanned ${candidates.length} journal notes, all already captured.`);
336
- console.log('');
486
+ print('');
487
+ print(` Scanned ${candidates.length} journal notes, all already captured.`);
488
+ print('');
337
489
  return;
338
490
  }
339
491
 
340
- console.log('');
341
- console.log(` Found ${fresh.length} new ${fresh.length === 1 ? 'note' : 'notes'} to harvest:`);
342
- console.log('');
492
+ print('');
493
+ print(` Found ${fresh.length} new ${fresh.length === 1 ? 'note' : 'notes'} to harvest:`);
494
+ print('');
343
495
  for (let i = 0; i < fresh.length; i++) {
344
496
  const c = fresh[i];
345
497
  const isPitfall = /^(don't|never|avoid|watch out|careful)/i.test(c.insight);
346
498
  const type = isPitfall ? 'pitfall' : 'pattern';
347
499
  const key = c.insight.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).slice(0, 4).join('-');
348
- console.log(` ${i + 1}. [${type}] ${c.insight}`);
349
- console.log(` from: ${c.source}`);
500
+ print(` ${i + 1}. [${type}] ${c.insight}`);
501
+ print(` from: ${c.source}`);
350
502
 
351
503
  try {
352
- addLearning({ type, key, insight: c.insight, confidence: 6, source: 'review', files: [] });
353
- console.log(` ✓ saved [6/10]`);
504
+ const entry = addLearning({ type, key, insight: c.insight, confidence: 6, source: 'review', files: [] });
505
+ print(` ✓ saved [6/10]`);
506
+ mintRichLearn({
507
+ cwd,
508
+ key: entry.key,
509
+ insight: entry.insight,
510
+ now: deps.now,
511
+ output: print,
512
+ });
354
513
  } catch (err) {
355
- console.log(` ✗ ${err.message}`);
514
+ print(` ✗ ${err.message}`);
356
515
  }
357
516
  }
358
- console.log('');
517
+ print('');
359
518
  }
360
519
 
361
520
  /**
@@ -375,10 +534,10 @@ function showLearnHelp() {
375
534
  console.log('');
376
535
  console.log(' Commands:');
377
536
  console.log(' (none) Show recent learnings');
378
- console.log(' add Add a learning interactively');
379
- console.log(' log <json> Add programmatically (for agents)');
537
+ console.log(' add Add a learning interactively. A rich insight mints one apply plus a failing measure.py. A thin insight prints check: fill this.');
538
+ console.log(' log <json> Add programmatically (for agents). A rich insight (number or named mechanism) mints one apply plus a failing measure.py. A thin insight prints check: fill this.');
380
539
  console.log(' search <q> Search learnings by keyword');
381
- console.log(' harvest Extract learnings from journal Notes');
540
+ console.log(' harvest Extract learnings from journal Notes. A rich insight mints one apply plus a failing measure.py. A thin insight prints check: fill this.');
382
541
  console.log(' prune Check for stale/contradictory entries');
383
542
  console.log(' stats Show learning statistics');
384
543
  console.log(' export Export as markdown');
@@ -437,5 +596,17 @@ function learnAtris(subcommand, ...args) {
437
596
  }
438
597
 
439
598
  learnAtris.getLearningCount = getLearningCount;
599
+ learnAtris.learnExperimentSlug = learnExperimentSlug;
600
+ learnAtris.learnExperimentRel = learnExperimentRel;
601
+ learnAtris.learnApplyRel = learnApplyRel;
602
+ learnAtris.learnLessonFromText = learnLessonFromText;
603
+ learnAtris.saveRichLearn = saveRichLearn;
604
+ learnAtris.ensureLearnApply = ensureLearnApply;
605
+ learnAtris.mintRichLearn = mintRichLearn;
606
+ learnAtris.commitAddedLearning = commitAddedLearning;
607
+ learnAtris.commitReviewLearning = commitReviewLearning;
608
+ learnAtris.reviewLearningKey = reviewLearningKey;
609
+ learnAtris.harvestFromJournals = harvestFromJournals;
610
+ learnAtris.logDirect = logDirect;
440
611
 
441
612
  module.exports = learnAtris;
@@ -10,6 +10,7 @@ const { defaultObjectiveRunner } = require('../lib/default-runner');
10
10
  const { readJson, writeJson } = require('../lib/json-file');
11
11
  const { hasFlag, readFlag, readNumberFlag } = require('../lib/arg-parser');
12
12
  const { ensureMemberBundle, memberBundlePresent } = require('../lib/member-scaffold');
13
+ const { memberProcessPrompt, MEMBER_PROCESS_PATH } = require('../lib/member-context');
13
14
 
14
15
  function findWorkspaceBusinessId(startDir = process.cwd()) {
15
16
  let dir = path.resolve(startDir);
@@ -462,6 +463,7 @@ function missionPurpose(paths) {
462
463
  }
463
464
 
464
465
  const MEMBER_RUN_RUNNABLE_STATUSES = new Set(['planning', 'running', 'ready']);
466
+ const MISSION_TERMINAL_STATUSES = new Set(['complete', 'stopped', 'failed']);
465
467
 
466
468
  function memberRunMissionMap() {
467
469
  try {
@@ -928,12 +930,11 @@ function memberPing(name, ...args) {
928
930
  process.exit(2);
929
931
  }
930
932
  const missionMod = require('./mission');
931
- const terminal = new Set(['complete', 'stopped', 'failed']);
932
933
  const candidates = [
933
934
  ...missionMod.listMissions(process.cwd()),
934
935
  ...missionMod.listWorktreeRollupMissions(process.cwd()),
935
936
  ]
936
- .filter((m) => m && m.owner === name && !terminal.has(m.status))
937
+ .filter((m) => m && m.owner === name && !MISSION_TERMINAL_STATUSES.has(m.status))
937
938
  .sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
938
939
 
939
940
  const taskNote = pingClaimedTaskDialogue(name, text, from);
@@ -4216,6 +4217,7 @@ function memberActivate(name) {
4216
4217
 
4217
4218
  const content = fs.readFileSync(activePath, 'utf8');
4218
4219
  const fm = parseFrontmatter(content) || {};
4220
+ const sharedProcess = memberProcessPrompt(process.cwd());
4219
4221
 
4220
4222
  console.log('');
4221
4223
  console.log(`Activating: ${fm.name || name} (${fm.role || 'no role'})`);
@@ -4298,7 +4300,8 @@ function memberActivate(name) {
4298
4300
 
4299
4301
  console.log('');
4300
4302
  console.log(`Member "${fm.name || name}" activated.`);
4301
- console.log(`Tell your agent: "You are the ${fm.role || name}. Read team/${name}/MEMBER.md."`);
4303
+ const identityPath = path.relative(process.cwd(), activePath);
4304
+ console.log(`Tell your agent: "You are the ${fm.role || name}. Read ${sharedProcess ? `${MEMBER_PROCESS_PATH}, then ` : ''}${identityPath}. Stay inside this member's permissions."`);
4302
4305
  }
4303
4306
 
4304
4307
  // --- UPGRADE subcommand ---
@@ -4707,6 +4710,49 @@ function memberGoalFromMission(name, ...args) {
4707
4710
  );
4708
4711
  return;
4709
4712
  }
4713
+ if (MISSION_TERMINAL_STATUSES.has(lowerCompact(runtime.status || ''))) {
4714
+ // A finished mission must not mint or re-point an active goal; that recreated the
4715
+ // stale-goal defect class (OBL-1961, OBL-2212, OBL-2232) after every retire. --force
4716
+ // cannot bypass this, it only widens the existing-goal match on a living mission.
4717
+ const staleGoals = state.goals.filter((goal) => (
4718
+ goal.source === 'mission' && goal.status === 'active'
4719
+ && runtime.id && goal.mission_id === runtime.id
4720
+ ));
4721
+ const staleList = staleGoals.map((goal) => goal.title || goal.id).join('; ');
4722
+ const ask = staleGoals.length
4723
+ ? `Mission ${runtime.id || compactSentence(runtimeFocus, 88)} is ${runtime.status}, so its active goal is stale. Retire ${compactSentence(staleList, 140)} by setting its status in goals.json, or start a living mission and run: atris member goal-from-mission ${name} --force`
4724
+ : `Mission ${runtime.id || compactSentence(runtimeFocus, 88)} is ${runtime.status}. Start a living mission, then run: atris member goal-from-mission ${name} --force`;
4725
+ const logPath = appendMemberGoalLog(paths.memberDir, name, 'Member goal-from-mission blocked', {
4726
+ ask,
4727
+ mission_id: runtime.id || '',
4728
+ mission_status: runtime.status || '',
4729
+ stale_goals: staleGoals.map((goal) => goal.id),
4730
+ });
4731
+ printJsonOrText(
4732
+ {
4733
+ ok: true,
4734
+ action: 'needs_user',
4735
+ member: name,
4736
+ needs_user: true,
4737
+ ask,
4738
+ stale_goals: staleGoals,
4739
+ mission: {
4740
+ north_star: purpose.northStar,
4741
+ runtime_id: runtime.id || null,
4742
+ runtime_status: runtime.status || null,
4743
+ runtime_next: runtime.next || null,
4744
+ },
4745
+ mission_file: paths.missionFile,
4746
+ log_path: logPath,
4747
+ },
4748
+ [
4749
+ `Blocked for ${name}: mission ${runtime.id || 'in now.md'} is ${runtime.status}, a finished mission cannot drive an active goal.`,
4750
+ `Ask: ${ask}`,
4751
+ ],
4752
+ asJson,
4753
+ );
4754
+ return;
4755
+ }
4710
4756
  // The title IS the mission focus, no boilerplate prefix; the acceptance list already
4711
4757
  // says "one bounded step" and the why carries the full sentence.
4712
4758
  const title = compactSentence(runtimeFocus, 96);
@@ -4977,7 +5023,8 @@ function fallbackProposalForGoal(goal, context = {}) {
4977
5023
  };
4978
5024
  }
4979
5025
 
4980
- function proposalPromptForGoal(goal, context = {}) {
5026
+ function proposalPromptForGoal(goal, context = {}, cwd = process.cwd()) {
5027
+ const sharedProcess = memberProcessPrompt(cwd);
4981
5028
  const files = (context?.evidence?.goal_files?.files || [])
4982
5029
  .filter((file) => file.exists && file.excerpt)
4983
5030
  .slice(0, 4)
@@ -5005,6 +5052,7 @@ function proposalPromptForGoal(goal, context = {}) {
5005
5052
  },
5006
5053
  };
5007
5054
  return [
5055
+ ...(sharedProcess ? [sharedProcess, ''] : []),
5008
5056
  'You generate the next bounded Atris member experiment.',
5009
5057
  'Read the JSON context and return only JSON with keys: title, proof_target, next_step, verifier, stop_rule.',
5010
5058
  'The next_step must be adaptive to the goal/evidence, concrete, receipt-backed, and safe for one bounded tick.',
@@ -5058,10 +5106,11 @@ async function callAtris2ProposalLlm(goal, context = {}) {
5058
5106
  const injected = injectedLlmProposal();
5059
5107
  if (injected) return injected;
5060
5108
  if (process.env.ATRIS_MEMBER_PROPOSAL_LLM !== '1') return null;
5109
+ const prompt = proposalPromptForGoal(goal, context, process.cwd());
5061
5110
  try {
5062
5111
  const { postTurn } = require('../ax');
5063
5112
  const output = { isTTY: false, write() { return true; } };
5064
- const result = await postTurn(proposalPromptForGoal(goal, context), {
5113
+ const result = await postTurn(prompt, {
5065
5114
  mode: process.env.ATRIS_MEMBER_PROPOSAL_LLM_MODE || 'fast',
5066
5115
  route: 'local',
5067
5116
  cwd: process.cwd(),
@@ -5761,6 +5810,7 @@ function emptyObjectiveGeneratorProposal(extra = {}) {
5761
5810
  updated_at: stampIso(),
5762
5811
  status: extra.status || 'empty',
5763
5812
  advisory_only: true,
5813
+ auto_task_eligible: extra.auto_task_eligible !== false,
5764
5814
  world_model_used: false,
5765
5815
  llm_source: extra.llm_source || null,
5766
5816
  llm_error: extra.llm_error || null,
@@ -5893,8 +5943,8 @@ function fallbackObjectiveGeneratorProposal(graph, recommendations, transferPatt
5893
5943
  ),
5894
5944
  suggested_member: member,
5895
5945
  suggested_patterns: objectivePatternMatches(transferPatterns, proposedObjective),
5896
- }, { status: 'ok', llm_error: 'llm_not_configured', world_model_used: true });
5897
- return proposal || emptyObjectiveGeneratorProposal({ status: 'llm_not_configured', llm_error: 'llm_not_configured' });
5946
+ }, { status: 'ok', llm_error: 'llm_not_configured', world_model_used: true, auto_task_eligible: false });
5947
+ return proposal || emptyObjectiveGeneratorProposal({ status: 'llm_not_configured', llm_error: 'llm_not_configured', auto_task_eligible: false });
5898
5948
  }
5899
5949
 
5900
5950
  function objectiveGeneratorPrompt(graph, recommendations, transferPatterns = []) {
@@ -6056,7 +6106,7 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
6056
6106
  proposal = emptyObjectiveGeneratorProposal({ status: llm.error === 'invalid_json' ? 'parse_error' : 'llm_error', llm_source: llm.source, llm_error: llm.error });
6057
6107
  proposal.world_model_used = true;
6058
6108
  } else {
6059
- reason = 'heuristic_objective_proposal_written';
6109
+ reason = execute ? 'heuristic_objective_proposal_written' : 'heuristic_objective_proposal_dry_run';
6060
6110
  proposal = fallbackObjectiveGeneratorProposal(graph, recommendations, transferPatterns);
6061
6111
  }
6062
6112
  }
@@ -6065,7 +6115,7 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
6065
6115
  proposal.suggested_patterns = objectivePatternMatches(transferPatterns, proposal.proposed_objective);
6066
6116
  }
6067
6117
 
6068
- if (execute && proposal?.status === 'ok' && Number(proposal.overall_score) > 7) {
6118
+ if (execute && proposal?.status === 'ok' && proposal.auto_task_eligible !== false && Number(proposal.overall_score) > 7) {
6069
6119
  createdTask = createAutoObjectiveTask(proposal);
6070
6120
  proposal.created_task = createdTask.ok ? {
6071
6121
  id: createdTask.task_id || null,
@@ -6081,7 +6131,8 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
6081
6131
  }
6082
6132
 
6083
6133
  const proposalsPath = objectiveGeneratorProposalsPath(root);
6084
- if (execute) {
6134
+ const proposalsWritten = Boolean(execute);
6135
+ if (proposalsWritten) {
6085
6136
  fs.mkdirSync(path.dirname(proposalsPath), { recursive: true });
6086
6137
  fs.writeFileSync(proposalsPath, JSON.stringify(proposal, null, 2) + '\n', 'utf8');
6087
6138
  }
@@ -6111,6 +6162,7 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
6111
6162
  llm_successful: Boolean(llm?.source && llm?.proposal && proposal.status === 'ok'),
6112
6163
  llm_error: llm?.error || proposal.llm_error || null,
6113
6164
  proposals_path: path.relative(root, proposalsPath),
6165
+ proposals_written: proposalsWritten,
6114
6166
  task_creation_threshold: 7,
6115
6167
  task_created: Boolean(createdTask?.ok),
6116
6168
  created_task: proposal.created_task,
@@ -6128,7 +6180,7 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
6128
6180
  score: proposal.overall_score || '',
6129
6181
  task: proposal.created_task?.ref || '',
6130
6182
  receipt: path.relative(root, receiptPath),
6131
- output: path.relative(root, proposalsPath),
6183
+ output: proposalsWritten ? path.relative(root, proposalsPath) : '',
6132
6184
  });
6133
6185
 
6134
6186
  return {
@@ -7817,6 +7869,7 @@ const WAKE_REASON_TEXT = {
7817
7869
  auto_improver_task_create_failed: 'it found an improvement but could not put the task on the board',
7818
7870
  heuristic_cross_domain_proof_written: 'it wrote a cross-domain proof using its built-in heuristics',
7819
7871
  heuristic_objective_proposal_written: 'it drafted an objective proposal using its built-in heuristics',
7872
+ heuristic_objective_proposal_dry_run: 'it previewed an objective proposal using its built-in heuristics, without writing it',
7820
7873
  install_requires_clean_git: 'installing needs a clean git tree first',
7821
7874
  insufficient_world_model_data: 'its world model is too thin to act on yet',
7822
7875
  llm_json_parse_failed: 'the model reply did not parse, so it stopped rather than act on garbage',
@@ -9232,6 +9285,7 @@ async function memberCommand(subcommand, ...args) {
9232
9285
  }
9233
9286
 
9234
9287
  module.exports = {
9288
+ proposalPromptForGoal,
9235
9289
  memberCommand,
9236
9290
  findAllMembers,
9237
9291
  findWorkspaceBusinessId,