atris 3.30.12 → 3.31.0

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 (58) hide show
  1. package/AGENTS.md +16 -3
  2. package/FOR_AGENTS.md +81 -0
  3. package/README.md +3 -1
  4. package/atris/atris.md +51 -19
  5. package/atris/skills/README.md +1 -0
  6. package/atris/skills/blocks/SKILL.md +134 -0
  7. package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
  8. package/atris/skills/youtube/SKILL.md +31 -11
  9. package/atris.md +7 -0
  10. package/ax +95 -3
  11. package/bin/atris.js +126 -153
  12. package/commands/autoland.js +319 -0
  13. package/commands/autopilot.js +94 -4
  14. package/commands/business.js +1 -1
  15. package/commands/clean.js +72 -9
  16. package/commands/codex-goal.js +72 -22
  17. package/commands/computer.js +48 -3
  18. package/commands/gm.js +1 -1
  19. package/commands/harvest.js +179 -0
  20. package/commands/init.js +1 -1
  21. package/commands/land.js +442 -0
  22. package/commands/loop-front.js +122 -4
  23. package/commands/member.js +519 -19
  24. package/commands/mission.js +3330 -290
  25. package/commands/play.js +1 -1
  26. package/commands/pulse.js +65 -7
  27. package/commands/run.js +3 -3
  28. package/commands/strings.js +301 -0
  29. package/commands/task.js +575 -102
  30. package/commands/truth.js +170 -0
  31. package/commands/xp.js +32 -8
  32. package/commands/youtube.js +72 -5
  33. package/decks/README.md +6 -12
  34. package/lib/auto-accept-certified.js +10 -0
  35. package/lib/autoland.js +283 -0
  36. package/lib/context-gatherer.js +0 -8
  37. package/lib/mission-artifact.js +504 -0
  38. package/lib/mission-room.js +846 -0
  39. package/lib/next-moves.js +212 -6
  40. package/lib/pulse.js +74 -1
  41. package/lib/runner-command.js +20 -8
  42. package/lib/runs-prune.js +242 -0
  43. package/lib/task-proof.js +1 -1
  44. package/package.json +3 -3
  45. package/decks/atris-seed-pitch-v3.json +0 -118
  46. package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
  47. package/decks/atris-seed-pitch-v5.json +0 -109
  48. package/decks/atris-seed-pitch-v6.json +0 -137
  49. package/decks/atris-seed-pitch-v7.json +0 -133
  50. package/decks/mark-pincus-narrative.json +0 -102
  51. package/decks/mark-pincus-sourcery.json +0 -94
  52. package/decks/yash-applied-compute-detailed.json +0 -150
  53. package/decks/yash-applied-compute-generalist.json +0 -82
  54. package/decks/yash-applied-compute-narrative.json +0 -54
  55. package/lib/ax-chat-input.js +0 -164
  56. package/lib/ax-goal.js +0 -307
  57. package/lib/ax-prefs.js +0 -63
  58. package/lib/ax-shimmer.js +0 -63
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const autoland = require('../lib/autoland');
9
+
10
+ function repoRoot(cwd = process.cwd()) {
11
+ const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' });
12
+ if (result.status !== 0) return cwd;
13
+ return result.stdout.trim() || cwd;
14
+ }
15
+
16
+ function projectName(root) {
17
+ return path.basename(root);
18
+ }
19
+
20
+ function flag(args, name, fallback = '') {
21
+ const idx = args.indexOf(name);
22
+ if (idx === -1) return fallback;
23
+ return args[idx + 1] || fallback;
24
+ }
25
+
26
+ function runOwnCli(root, cliArgs) {
27
+ const bin = path.join(root, 'bin', 'atris.js');
28
+ const argv = fs.existsSync(bin) ? [process.execPath, [bin, ...cliArgs]] : ['atris', [cliArgs].flat()];
29
+ const result = spawnSync(argv[0], argv[1], { cwd: root, encoding: 'utf8', timeout: 300000 });
30
+ return { status: result.status, stdout: String(result.stdout || ''), stderr: String(result.stderr || '') };
31
+ }
32
+
33
+ function readProjection(root) {
34
+ try {
35
+ const raw = fs.readFileSync(path.join(root, '.atris', 'state', 'tasks.projection.json'), 'utf8');
36
+ const projection = JSON.parse(raw);
37
+ return Array.isArray(projection.tasks) ? projection.tasks : [];
38
+ } catch (err) {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ function landSummarySafe(root) {
44
+ try {
45
+ return require('./land').landSummary(root);
46
+ } catch (err) {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function evaluateQueue(root, { strictVerify }) {
52
+ const cliArgs = ['task', 'auto-accept-certified', '--dry-run', '--json', '--limit', '50'];
53
+ if (strictVerify) cliArgs.push('--strict-verify');
54
+ const result = runOwnCli(root, cliArgs);
55
+ try {
56
+ const parsed = JSON.parse(result.stdout);
57
+ return Array.isArray(parsed.results) ? parsed.results : [];
58
+ } catch (err) {
59
+ return [];
60
+ }
61
+ }
62
+
63
+ function plainReason(reason) {
64
+ const map = {
65
+ denied_tag_billing: 'money — yours to approve',
66
+ denied_tag_deploy: 'a deploy — yours to approve',
67
+ denied_tag_security: 'security — yours to approve',
68
+ denied_tag_customer: 'customer-facing — yours to approve',
69
+ denied_tag_external: 'outward-facing — yours to approve',
70
+ denied_tag_feedback: 'customer feedback — yours to approve',
71
+ denied_tag_voice: 'voice/comms — yours to approve',
72
+ needs_second_reviewer_or_third_pass: 'needs one more independent check first',
73
+ not_agent_certified: 'not certified yet',
74
+ insufficient_review_passes: 'not enough review passes yet',
75
+ strict_verify_missing: 'no recorded check command to re-run',
76
+ verify_failed: 'its check command failed on re-run',
77
+ proof_unmerged_or_draft_pr_boundary: 'its proof points at an unmerged draft',
78
+ };
79
+ return map[reason] || reason.replace(/_/g, ' ');
80
+ }
81
+
82
+ function showStatus(root, args) {
83
+ const json = args.includes('--json');
84
+ const policy = autoland.readPolicy(root);
85
+ const enabled = Boolean(policy && policy.enabled);
86
+ const strictVerify = policy ? policy.strict_verify !== false : true;
87
+ const results = evaluateQueue(root, { strictVerify })
88
+ .filter((r) => r.reason !== 'not_in_review');
89
+ const wouldLand = results.filter((r) => r.action === 'would_accept');
90
+ const blocked = results.filter((r) => r.action !== 'would_accept');
91
+ const tasks = readProjection(root);
92
+ const waiting = autoland.waitingOnHuman(tasks);
93
+
94
+ if (json) {
95
+ console.log(JSON.stringify({
96
+ enabled,
97
+ policy,
98
+ heartbeat_installed: autoland.cronInstalled(root),
99
+ would_land: wouldLand,
100
+ blocked,
101
+ waiting_on_human: waiting,
102
+ }, null, 2));
103
+ return 0;
104
+ }
105
+
106
+ console.log('');
107
+ console.log(`autoland — certified work lands itself; you keep the irreversible calls`);
108
+ console.log('');
109
+ console.log(` policy: ${enabled ? `on (flipped by ${policy.enabled_by}${policy.enabled_at ? ` on ${String(policy.enabled_at).slice(0, 10)}` : ''})` : 'off — everything waits for you'}`);
110
+ console.log(` heartbeat: ${autoland.cronInstalled(root) ? 'running hourly' : 'not installed'}`);
111
+ if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
112
+ console.log('');
113
+ if (wouldLand.length > 0) {
114
+ console.log(` ready to land on their own: ${wouldLand.length}`);
115
+ for (const r of wouldLand.slice(0, 10)) console.log(` lands itself ${r.ref}`);
116
+ } else {
117
+ console.log(' nothing is ready to land on its own right now.');
118
+ }
119
+ const humanOnly = blocked.filter((r) => String(r.reason || '').startsWith('denied_tag_'));
120
+ const needsWork = blocked.filter((r) => !String(r.reason || '').startsWith('denied_tag_'));
121
+ if (humanOnly.length > 0) {
122
+ console.log(` yours to approve (protected lanes): ${humanOnly.length}`);
123
+ for (const r of humanOnly.slice(0, 10)) console.log(` waits for you ${r.ref} — ${plainReason(r.reason)}`);
124
+ }
125
+ if (needsWork.length > 0) {
126
+ console.log(` not ready yet: ${needsWork.length}`);
127
+ for (const r of needsWork.slice(0, 10)) console.log(` held back ${r.ref} — ${plainReason(String(r.reason || ''))}`);
128
+ }
129
+ if (waiting.length > 0) {
130
+ console.log('');
131
+ console.log(` waiting on a human right now: ${waiting.length}, oldest ${waiting[0].hours}h`);
132
+ }
133
+ console.log('');
134
+ if (!enabled) console.log(" flip it on: atris autoland on (one decision; everything after is receipts)");
135
+ console.log('');
136
+ return 0;
137
+ }
138
+
139
+ function turnOn(root, args) {
140
+ const owner = flag(args, '--as', os.userInfo().username);
141
+ const to = flag(args, '--to', '');
142
+ const digestHour = Number(flag(args, '--digest-hour', autoland.DEFAULT_DIGEST_HOUR));
143
+ const alarmHours = Number(flag(args, '--alarm-hours', autoland.DEFAULT_ALARM_HOURS));
144
+ const policy = autoland.writePolicy(root, {
145
+ enabled: true,
146
+ enabled_by: owner,
147
+ enabled_at: new Date().toISOString(),
148
+ imessage_to: to || null,
149
+ digest_hour: Number.isFinite(digestHour) ? digestHour : autoland.DEFAULT_DIGEST_HOUR,
150
+ alarm_hours: Number.isFinite(alarmHours) && alarmHours > 0 ? alarmHours : autoland.DEFAULT_ALARM_HOURS,
151
+ strict_verify: !args.includes('--no-strict-verify'),
152
+ });
153
+ const cronOk = autoland.installCron(root);
154
+ console.log('');
155
+ console.log('autoland is on.');
156
+ console.log(` certified, verified, reversible work now lands itself, accepted as ${owner}.`);
157
+ console.log(' protected lanes (money, deploys, security, customer, outward) still wait for you.');
158
+ console.log(` heartbeat: ${cronOk ? 'installed, runs hourly' : 'could not install cron — run atris autoland tick yourself'}`);
159
+ if (policy.imessage_to) {
160
+ console.log(` daily message to ${policy.imessage_to} at ${policy.digest_hour}:00; anything waiting on you past ${policy.alarm_hours}h pings you.`);
161
+ } else {
162
+ console.log(' no phone number set — digest goes to the log only. add one: atris autoland on --to <your number>');
163
+ }
164
+ console.log(' turn it off any time: atris autoland off');
165
+ console.log('');
166
+ return 0;
167
+ }
168
+
169
+ function turnOff(root) {
170
+ const policy = autoland.readPolicy(root) || {};
171
+ autoland.writePolicy(root, { ...policy, enabled: false, disabled_at: new Date().toISOString() });
172
+ const cronOk = autoland.uninstallCron(root);
173
+ console.log('');
174
+ console.log('autoland is off. everything waits for your accept again.');
175
+ console.log(` heartbeat ${cronOk ? 'removed' : 'removal failed — check crontab -l'}.`);
176
+ console.log('');
177
+ return 0;
178
+ }
179
+
180
+ function runDigest(root, args, { forceSend = false } = {}) {
181
+ const policy = autoland.readPolicy(root) || {};
182
+ const tasks = readProjection(root);
183
+ const accepted = autoland.acceptedInLastDay(tasks);
184
+ const text = autoland.composeDigest({
185
+ accepted,
186
+ waiting: autoland.waitingOnHuman(tasks),
187
+ landed: landSummarySafe(root),
188
+ project: projectName(root),
189
+ });
190
+ console.log(text);
191
+ // the full story: what each piece actually was, in its own words
192
+ const byRef = new Map(tasks.map((t) => [t.display_id || t.legacy_ref || t.id, t]));
193
+ const storied = accepted.auto.filter((item) => {
194
+ const t = byRef.get(item.ref);
195
+ return t && (t.review?.landing?.happened || t.metadata?.landing_happened);
196
+ });
197
+ if (storied.length > 0) {
198
+ console.log('');
199
+ for (const item of storied) {
200
+ const t = byRef.get(item.ref);
201
+ const happened = String(t.review?.landing?.happened || t.metadata?.landing_happened || '').replace(/\s+/g, ' ').slice(0, 160);
202
+ console.log(` ${item.ref} ${happened}`);
203
+ }
204
+ }
205
+ const shouldSend = (forceSend || args.includes('--send')) && policy.imessage_to;
206
+ if (shouldSend) {
207
+ const sent = autoland.sendImessage(root, policy.imessage_to, text);
208
+ console.log(sent.ok ? `(sent to ${policy.imessage_to})` : `(send failed: ${sent.output})`);
209
+ return sent.ok ? 0 : 1;
210
+ }
211
+ return 0;
212
+ }
213
+
214
+ function runTick(root, args) {
215
+ const json = args.includes('--json');
216
+ const policy = autoland.readPolicy(root);
217
+ const receipt = { at: new Date().toISOString(), landed: [], alarms: 0, digest_sent: false, enabled: Boolean(policy && policy.enabled) };
218
+ if (!policy || policy.enabled !== true) {
219
+ if (json) console.log(JSON.stringify(receipt));
220
+ else console.log('autoland is off; tick did nothing.');
221
+ return 0;
222
+ }
223
+
224
+ // 1. land what is eligible — the policy is the standing authorization
225
+ const cliArgs = ['task', 'auto-accept-certified', '--json', '--limit', '12'];
226
+ if (policy.strict_verify !== false) cliArgs.push('--strict-verify');
227
+ const accept = runOwnCli(root, cliArgs);
228
+ try {
229
+ const parsed = JSON.parse(accept.stdout);
230
+ receipt.landed = (parsed.results || []).filter((r) => r.action === 'accepted').map((r) => r.ref);
231
+ } catch (err) {
232
+ receipt.accept_error = accept.stderr.slice(0, 200) || 'auto-accept output unreadable';
233
+ }
234
+
235
+ // 2. alarm on anything waiting on a human past the line
236
+ const state = autoland.readState(root);
237
+ const tasks = readProjection(root);
238
+ const waiting = autoland.waitingOnHuman(tasks);
239
+ const alarmHours = Number(policy.alarm_hours) || autoland.DEFAULT_ALARM_HOURS;
240
+ const due = autoland.dueForAlarm(waiting, state, { alarmHours });
241
+ if (due.length > 0 && policy.imessage_to) {
242
+ const text = autoland.composeAlarm({ waiting: due, project: projectName(root), alarmHours });
243
+ const sent = autoland.sendImessage(root, policy.imessage_to, text);
244
+ if (sent.ok) {
245
+ autoland.markAlerted(state, due);
246
+ receipt.alarms = due.length;
247
+ }
248
+ }
249
+
250
+ // 3. daily digest at the configured hour
251
+ const today = new Date().toISOString().slice(0, 10);
252
+ const digestHour = Number(policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR);
253
+ if (new Date().getHours() === digestHour && state.last_digest_date !== today) {
254
+ const text = autoland.composeDigest({
255
+ accepted: autoland.acceptedInLastDay(tasks),
256
+ waiting,
257
+ landed: landSummarySafe(root),
258
+ project: projectName(root),
259
+ });
260
+ if (policy.imessage_to) {
261
+ const sent = autoland.sendImessage(root, policy.imessage_to, text);
262
+ receipt.digest_sent = sent.ok;
263
+ }
264
+ receipt.digest_text = text;
265
+ state.last_digest_date = today;
266
+
267
+ // 4. once a day, keep the receipt shelf lean: compress old run receipts
268
+ // into the manifest and drop unreferenced clutter, newest 200 kept.
269
+ const prune = runOwnCli(root, ['mission', 'prune-runs', '--apply', '--days', '14', '--keep-newest', '200', '--json']);
270
+ try {
271
+ const pruned = JSON.parse(prune.stdout);
272
+ receipt.receipts_pruned = pruned.pruned_count ?? pruned.pruned ?? pruned.removed ?? 0;
273
+ } catch {
274
+ receipt.receipts_pruned = null;
275
+ }
276
+ }
277
+ autoland.writeState(root, state);
278
+
279
+ if (json) console.log(JSON.stringify(receipt));
280
+ else {
281
+ console.log(`autoland tick: ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms, digest ${receipt.digest_sent ? 'sent' : 'not due'}`);
282
+ }
283
+ return 0;
284
+ }
285
+
286
+ function showHelp() {
287
+ console.log('');
288
+ console.log('atris autoland — you approve the policy once; certified work lands itself');
289
+ console.log('');
290
+ console.log('finished work that passed its checks and two independent reviews lands');
291
+ console.log('automatically with a receipt. money, deploys, security, customer, and');
292
+ console.log('outward-facing work always waits for you.');
293
+ console.log('');
294
+ console.log(' atris autoland what would land, what waits for you');
295
+ console.log(' atris autoland on [--to <phone>] flip it on: hourly heartbeat, daily');
296
+ console.log(' message, ping when something waits');
297
+ console.log(' on you past 24h');
298
+ console.log(' atris autoland off back to approving every item');
299
+ console.log(' atris autoland digest [--send] the daily message, now');
300
+ console.log(' atris autoland tick one heartbeat (what the hourly cron runs)');
301
+ console.log('');
302
+ return 0;
303
+ }
304
+
305
+ function autolandCommand(args = []) {
306
+ const [sub, ...rest] = args;
307
+ if (!sub || sub.startsWith('--')) return showStatus(repoRoot(), args);
308
+ if (sub === 'help' || sub === '--help' || sub === '-h') return showHelp();
309
+ const root = repoRoot();
310
+ if (sub === 'on') return turnOn(root, rest);
311
+ if (sub === 'off') return turnOff(root);
312
+ if (sub === 'tick') return runTick(root, rest);
313
+ if (sub === 'digest') return runDigest(root, rest);
314
+ if (sub === 'status') return showStatus(root, rest);
315
+ console.error(`atris autoland: unknown subcommand '${sub}' (try: atris autoland help)`);
316
+ return 1;
317
+ }
318
+
319
+ module.exports = { autolandCommand };
@@ -15,7 +15,7 @@ const { parseTodo } = require('../lib/todo');
15
15
  const {
16
16
  buildRunnerCommand,
17
17
  buildRunnerAvailabilityCommand,
18
- resolveClaudeRunnerBin,
18
+ runnerAvailabilityFailureMessage,
19
19
  } = require('../lib/runner-command');
20
20
  const { findStalePages, findStaleTasks, healBrokenMapRefs } = require('./clean');
21
21
  const {
@@ -484,6 +484,91 @@ function executePhaseDetailed(phase, context, options = {}) {
484
484
  }
485
485
  }
486
486
 
487
+ function autopilotGlassLogDir(cwd = process.cwd()) {
488
+ const dir = path.join(cwd, 'atris', 'logs', 'autopilot');
489
+ fs.mkdirSync(dir, { recursive: true });
490
+ return dir;
491
+ }
492
+
493
+ function autopilotGlassSlug(value) {
494
+ const slug = String(value || 'tick')
495
+ .toLowerCase()
496
+ .replace(/[^a-z0-9]+/g, '-')
497
+ .replace(/^-+|-+$/g, '')
498
+ .slice(0, 64);
499
+ return slug || 'tick';
500
+ }
501
+
502
+ function autopilotGlassTimestamp(now = new Date()) {
503
+ return now.toISOString().replace(/[:.]/g, '-');
504
+ }
505
+
506
+ function phaseGlassText(phaseResults, key) {
507
+ const phase = phaseResults && phaseResults[key] ? phaseResults[key] : {};
508
+ const output = String(phase.output || '').trim();
509
+ const elapsed = Number.isFinite(phase.elapsedSeconds) ? `\n\nElapsed: ${phase.elapsedSeconds}s` : '';
510
+ return `${output || '(no output)'}${elapsed}`;
511
+ }
512
+
513
+ function buildAutopilotGlassLog(context, execution, options = {}) {
514
+ const phaseResults = execution.phaseResults || {};
515
+ const task = context.task || 'Autopilot tick';
516
+ const generatedAt = options.generatedAt || new Date().toISOString();
517
+ const proof = execution.verifyRan
518
+ ? `${execution.verifyPass ? 'PASS' : 'FAIL'}: ${phaseGlassText(phaseResults, 'verify')}`
519
+ : 'NOT RUN: no verifier command was available';
520
+
521
+ return [
522
+ '# Autopilot Glass Log',
523
+ '',
524
+ `Generated: ${generatedAt}`,
525
+ `Task: ${task}`,
526
+ `Kind: ${context.kind || 'unknown'}`,
527
+ `Verify: ${execution.verifyCmd || '(none)'}`,
528
+ `Outcome: ${execution.success ? 'success' : 'needs attention'}`,
529
+ '',
530
+ '## Plan',
531
+ '',
532
+ phaseGlassText(phaseResults, 'plan'),
533
+ '',
534
+ '## Plan Review',
535
+ '',
536
+ phaseGlassText(phaseResults, 'plan-review'),
537
+ '',
538
+ '## Action',
539
+ '',
540
+ phaseGlassText(phaseResults, 'do'),
541
+ '',
542
+ '## Result',
543
+ '',
544
+ phaseGlassText(phaseResults, 'review'),
545
+ '',
546
+ '## Proof',
547
+ '',
548
+ proof,
549
+ ''
550
+ ].join('\n');
551
+ }
552
+
553
+ function writeAutopilotGlassLog(cwd, context, execution, options = {}) {
554
+ const dir = autopilotGlassLogDir(cwd);
555
+ const now = options.now || new Date();
556
+ const file = `${autopilotGlassTimestamp(now)}-${autopilotGlassSlug(context.task)}.md`;
557
+ const absPath = path.join(dir, file);
558
+ fs.writeFileSync(absPath, buildAutopilotGlassLog(context, execution, {
559
+ generatedAt: now.toISOString(),
560
+ }));
561
+ return path.relative(cwd, absPath);
562
+ }
563
+
564
+ function maybeWriteAutopilotGlassLog(cwd, context, execution) {
565
+ try {
566
+ return writeAutopilotGlassLog(cwd, context, execution);
567
+ } catch {
568
+ return null;
569
+ }
570
+ }
571
+
487
572
  /**
488
573
  * Build context-aware file list for prompts.
489
574
  */
@@ -1806,7 +1891,7 @@ function runTaskOnce(context, options = {}) {
1806
1891
  }
1807
1892
  }
1808
1893
 
1809
- return {
1894
+ const execution = {
1810
1895
  success: verifyRan && verifyPass,
1811
1896
  elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
1812
1897
  phaseResults,
@@ -1815,6 +1900,8 @@ function runTaskOnce(context, options = {}) {
1815
1900
  verifyPass,
1816
1901
  verifyRan,
1817
1902
  };
1903
+ execution.glassLogPath = maybeWriteAutopilotGlassLog(cwd, context, execution);
1904
+ return execution;
1818
1905
  }
1819
1906
 
1820
1907
  /**
@@ -3022,8 +3109,8 @@ async function autopilotAtris(description, options = {}) {
3022
3109
  process.exit(1);
3023
3110
  }
3024
3111
 
3025
- try { execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' }); } catch {
3026
- console.error(`${resolveClaudeRunnerBin()} CLI not found. Set ATRIS_RUNNER_BIN (or legacy ATRIS_CLAUDE_BIN), or install the configured runner first.`);
3112
+ try { execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' }); } catch (err) {
3113
+ console.error(runnerAvailabilityFailureMessage(err));
3027
3114
  process.exit(1);
3028
3115
  }
3029
3116
 
@@ -3679,5 +3766,8 @@ module.exports = {
3679
3766
  isPhaseKillError,
3680
3767
  execPhaseCommandSync,
3681
3768
  executePhaseDetailed,
3769
+ autopilotGlassLogDir,
3770
+ buildAutopilotGlassLog,
3771
+ writeAutopilotGlassLog,
3682
3772
  lessonSlug
3683
3773
  };
@@ -840,7 +840,7 @@ function renderBusinessOsLines(os = {}, prefix = '- ') {
840
840
  `${prefix}Team goals: ${team.members || 0} member lanes, ${team.activeGoalMembers || 0} with active goals`,
841
841
  `${prefix}${xp.metric || 'AgentXP'}: ${xp.total || 0} total, ${xp.today || 0} today, ${xp.receipts || 0} receipts, integrity ${xp.integrity || 'unknown'}`,
842
842
  `${prefix}Loop: ${loop.ticks || 0} mission ticks; Codex goal ${loop.codexGoal || 'none'}`,
843
- `${prefix}XP gate: proof can move to Review; XP lands only after human accept`,
843
+ `${prefix}XP gate: proof can move to Review; XP is awarded only after human approval`,
844
844
  ];
845
845
  }
846
846
 
package/commands/clean.js CHANGED
@@ -15,20 +15,23 @@ function cleanAtris(options = {}) {
15
15
  const atrisDir = path.join(cwd, 'atris');
16
16
 
17
17
  if (!fs.existsSync(atrisDir)) {
18
+ if (options.json) {
19
+ console.log(JSON.stringify({
20
+ ok: false,
21
+ action: 'clean',
22
+ error: 'atris/ folder not found. Run "atris init" first.',
23
+ }, null, 2));
24
+ process.exit(1);
25
+ }
18
26
  console.log('✗ atris/ folder not found. Run "atris init" first.');
19
27
  process.exit(1);
20
28
  }
21
29
 
22
- console.log('');
23
- console.log('┌─────────────────────────────────────────────────────────────┐');
24
- console.log('│ Atris Clean │');
25
- console.log('└─────────────────────────────────────────────────────────────┘');
26
- console.log('');
27
-
28
30
  const results = {
29
31
  staleTasks: [],
30
32
  brokenRefs: [],
31
33
  healedRefs: 0,
34
+ healedRefDetails: [],
32
35
  unhealableRefs: [],
33
36
  archivedJournals: 0,
34
37
  cleanedSections: 0
@@ -39,8 +42,9 @@ function cleanAtris(options = {}) {
39
42
  results.staleTasks = staleTasks;
40
43
 
41
44
  // 2. Find and HEAL broken MAP.md references
42
- const { healed, unhealable } = healBrokenMapRefs(cwd, atrisDir, options.dryRun);
45
+ const { healed, unhealable, replacements } = healBrokenMapRefs(cwd, atrisDir, options.dryRun);
43
46
  results.healedRefs = healed;
47
+ results.healedRefDetails = replacements;
44
48
  results.unhealableRefs = unhealable;
45
49
 
46
50
  // 3. Archive old journals (>30 days)
@@ -55,6 +59,17 @@ function cleanAtris(options = {}) {
55
59
  const stalePages = findStalePages(cwd, atrisDir);
56
60
  results.stalePages = stalePages;
57
61
 
62
+ if (options.json) {
63
+ console.log(JSON.stringify(cleanResultPayload(results, options, cwd), null, 2));
64
+ return results;
65
+ }
66
+
67
+ console.log('');
68
+ console.log('┌─────────────────────────────────────────────────────────────┐');
69
+ console.log('│ Atris Clean │');
70
+ console.log('└─────────────────────────────────────────────────────────────┘');
71
+ console.log('');
72
+
58
73
  // Report results
59
74
  console.log('Results:');
60
75
  console.log('');
@@ -74,6 +89,12 @@ function cleanAtris(options = {}) {
74
89
  if (healed > 0) {
75
90
  const verb = options.dryRun ? 'Would heal' : 'Healed';
76
91
  console.log(`✓ ${verb} ${healed} MAP.md ${healed === 1 ? 'reference' : 'references'}`);
92
+ (results.healedRefDetails || []).slice(0, 3).forEach(ref => {
93
+ console.log(` • ${ref.old} -> ${ref.new}${ref.symbol ? ` (${ref.symbol})` : ''}`);
94
+ });
95
+ if ((results.healedRefDetails || []).length > 3) {
96
+ console.log(` ... and ${results.healedRefDetails.length - 3} more`);
97
+ }
77
98
  }
78
99
 
79
100
  // Unhealable refs
@@ -138,6 +159,47 @@ function cleanAtris(options = {}) {
138
159
  return results;
139
160
  }
140
161
 
162
+ function cleanResultPayload(results, options = {}, cwd = process.cwd()) {
163
+ const manualAction = [];
164
+ if (results.staleTasks.length > 0) manualAction.push('Delete stale tasks or finish them');
165
+ if (results.unhealableRefs.length > 0) manualAction.push('Manually fix unhealable MAP.md refs');
166
+ if (results.stalePages.length > 0) manualAction.push('Re-compile stale pages');
167
+ return {
168
+ ok: manualAction.length === 0,
169
+ action: 'clean',
170
+ dry_run: !!options.dryRun,
171
+ results: {
172
+ stale_tasks: {
173
+ count: results.staleTasks.length,
174
+ items: results.staleTasks,
175
+ },
176
+ map_refs: {
177
+ healed_count: results.healedRefs,
178
+ verb: options.dryRun ? 'would_heal' : 'healed',
179
+ items: results.healedRefDetails || [],
180
+ unhealable_count: results.unhealableRefs.length,
181
+ unhealable: results.unhealableRefs,
182
+ },
183
+ journals: {
184
+ archived_count: results.archivedJournals,
185
+ verb: options.dryRun ? 'would_archive' : 'archived',
186
+ },
187
+ sections: {
188
+ cleaned_count: results.cleanedSections,
189
+ verb: options.dryRun ? 'would_clean' : 'cleaned',
190
+ },
191
+ stale_pages: {
192
+ count: results.stalePages.length,
193
+ items: results.stalePages.map(page => ({
194
+ page: path.relative(cwd, page.page),
195
+ stale_source: page.staleSource,
196
+ })),
197
+ },
198
+ },
199
+ manual_action: manualAction,
200
+ };
201
+ }
202
+
141
203
  /**
142
204
  * Find tasks claimed but not completed after 3+ days
143
205
  */
@@ -295,7 +357,7 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
295
357
  fs.writeFileSync(mapFile, mapContent);
296
358
  }
297
359
 
298
- return { healed, unhealable, replacements: dryRun ? replacements : [] };
360
+ return { healed, unhealable, replacements };
299
361
  }
300
362
 
301
363
  /**
@@ -337,7 +399,7 @@ function findFunctionEnd(lines, startLine) {
337
399
  }
338
400
 
339
401
  /**
340
- * Extract a symbol name from context like "(atrisDevEntry function)" or "— Main entry"
402
+ * Extract a symbol name from context like "(interactiveEntry function)" or "— Main entry"
341
403
  */
342
404
  function extractSymbol(context) {
343
405
  if (!context) return null;
@@ -568,6 +630,7 @@ function cleanEmptySections(atrisDir, dryRun = false) {
568
630
 
569
631
  module.exports = {
570
632
  cleanAtris,
633
+ cleanResultPayload,
571
634
  findStaleTasks,
572
635
  healBrokenMapRefs,
573
636
  archiveOldJournals,