atris 3.15.56 → 3.16.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 (44) hide show
  1. package/AGENTS.md +2 -2
  2. package/GETTING_STARTED.md +1 -1
  3. package/PERSONA.md +4 -4
  4. package/README.md +11 -11
  5. package/atris/skills/copy-editor/SKILL.md +30 -4
  6. package/atris/skills/improve/SKILL.md +18 -20
  7. package/atris/wiki/concepts/agent-activation-contract.md +5 -3
  8. package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
  9. package/atris/wiki/index.md +1 -0
  10. package/ax +522 -73
  11. package/bin/atris.js +32 -31
  12. package/commands/align.js +0 -14
  13. package/commands/apps.js +102 -1
  14. package/commands/autopilot.js +197 -22
  15. package/commands/brain.js +219 -34
  16. package/commands/brainstorm.js +0 -829
  17. package/commands/computer.js +45 -83
  18. package/commands/improve.js +501 -0
  19. package/commands/integrations.js +228 -0
  20. package/commands/lesson.js +44 -0
  21. package/commands/member.js +4498 -226
  22. package/commands/mission.js +302 -27
  23. package/commands/now.js +89 -1
  24. package/commands/radar.js +181 -56
  25. package/commands/skill.js +37 -6
  26. package/commands/soul.js +0 -4
  27. package/commands/task.js +5582 -517
  28. package/commands/terminal.js +14 -10
  29. package/commands/wiki.js +87 -1
  30. package/commands/workflow.js +288 -73
  31. package/commands/worktree.js +52 -15
  32. package/commands/xp.js +41 -65
  33. package/lib/auto-accept-certified.js +294 -0
  34. package/lib/file-ops.js +0 -184
  35. package/lib/member-alive.js +232 -0
  36. package/lib/policy-lessons.js +280 -0
  37. package/lib/receipt-evidence.js +64 -0
  38. package/lib/state-detection.js +34 -0
  39. package/lib/task-db.js +568 -16
  40. package/lib/task-proof.js +43 -0
  41. package/package.json +1 -1
  42. package/utils/auth.js +13 -4
  43. package/commands/research.js +0 -52
  44. package/lib/section-merge.js +0 -196
package/bin/atris.js CHANGED
@@ -67,8 +67,8 @@ const ensureValidCredentials = (opts) => _ensureValidCredentials(apiRequestJson,
67
67
  const fetchMyAgents = (token) => _fetchMyAgents(token, apiRequestJson);
68
68
  const displayAccountSummary = () => _displayAccountSummary(apiRequestJson);
69
69
 
70
- // Run update check in background (non-blocking)
71
- // Skip for 'version', 'update', and help commands to avoid redundant messages or help side effects.
70
+ // Run update check in background (non-blocking).
71
+ // Skip for machine-readable JSON and help commands to avoid corrupting stdout.
72
72
  let updateCheckPromise = null;
73
73
  const updateCommand = process.argv[2];
74
74
  const updateArgs = process.argv.slice(3);
@@ -78,7 +78,8 @@ const helpRequested = updateCommand === 'help'
78
78
  || updateArgs.includes('--help')
79
79
  || updateArgs.includes('-h')
80
80
  || updateArgs[0] === 'help';
81
- const skipUpdateCheck = Boolean(process.env.ATRIS_SKIP_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER || helpRequested);
81
+ const jsonRequested = updateArgs.includes('--json');
82
+ const skipUpdateCheck = Boolean(process.env.ATRIS_SKIP_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER || helpRequested || jsonRequested);
82
83
  if (!skipUpdateCheck && (!updateCommand || (updateCommand && !['version', 'update'].includes(updateCommand)))) {
83
84
  updateCheckPromise = checkForUpdates()
84
85
  .then((updateInfo) => {
@@ -117,15 +118,6 @@ const isBusinessSyncSafetyCommand = command === 'sync'
117
118
  || firstCommandArg === 'resolve'
118
119
  );
119
120
 
120
- // Keep APP.md app-pack operations independent from the heavier workspace boot
121
- // path so `atris apps --json` stays machine-readable for agents.
122
- if (command === 'apps') {
123
- const subcommand = process.argv[3];
124
- const args = process.argv.slice(4);
125
- require('../commands/apps').appsCommand(subcommand, ...args);
126
- process.exit(0);
127
- }
128
-
129
121
  // Auto-sync skills only for commands that modify workspace state
130
122
  if (['init', 'update', 'upgrade'].includes(command) || (command === 'sync' && !isBusinessSyncSafetyCommand)) {
131
123
  try {
@@ -346,7 +338,7 @@ function showHelp() {
346
338
  console.log(' release - Tag release, bump version, create GitHub release, draft /launch');
347
339
  console.log(' learn - Project learnings (patterns, pitfalls, preferences)');
348
340
  console.log(' brain - Compile MAP/TODO/wiki/state into a loadable agent brain');
349
- console.log(' lesson - Append a one-line lesson to atris/lessons.md');
341
+ console.log(' lesson - Append a one-line lesson to atris/lessons.md (mine: distill receipts/episodes/scorecards into policy lessons)');
350
342
  console.log(' ingest - Local-first wiki ingest into atris/wiki/');
351
343
  console.log(' query - Local-first wiki query against atris/wiki/');
352
344
  console.log(' lint - Local-first wiki lint for atris/wiki/');
@@ -355,6 +347,7 @@ function showHelp() {
355
347
  console.log('Optional helpers:');
356
348
  console.log(' brainstorm - Explore ideas conversationally before planning');
357
349
  console.log(' autopilot - Guided loop that can clarify TODOs and run plan → do → review');
350
+ console.log(' improve - Run one paid RL tick (POST /api/improve, deducts credits)');
358
351
  console.log(' worktree - Isolated Git worktrees plus guarded ship/merge for parallel agents');
359
352
  console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
360
353
  console.log('');
@@ -491,14 +484,18 @@ function showDoHelp() {
491
484
 
492
485
  function showReviewHelp() {
493
486
  console.log('');
494
- console.log('Usage: atris review [--execute] [--full]');
487
+ console.log('Usage: atris review [--limit N|--all|--json] [--full|--execute]');
495
488
  console.log('');
496
489
  console.log('Description:');
497
- console.log(' Activate the Validator agent to verify recent changes.');
498
- console.log(' Reads TODO.md, MAP.md, and today\'s journal, then prints a validation');
499
- console.log(' checklist (and, in agent mode, runs tests and updates docs).');
490
+ console.log(' Show the certified Review queue: proof-ready work waiting for');
491
+ console.log(' human accept or revise. Human accept is the AgentXP gate.');
492
+ console.log(' Use --full/--verbose for the legacy Validator prompt.');
500
493
  console.log('');
501
494
  console.log('Options:');
495
+ console.log(' --limit N Show at most N certified review rows.');
496
+ console.log(' --all Show all certified review rows.');
497
+ console.log(' --json Emit the task-backed review queue as JSON.');
498
+ console.log(' --group-by Group certified rows by tag, owner, or source.');
502
499
  console.log(' --execute Run in agent mode via Atris cloud (requires login + agent).');
503
500
  console.log(' --full Print full spec/context dumps (verbose copy/paste).');
504
501
  console.log(' --verbose Alias for --full.');
@@ -769,7 +766,7 @@ if (command === '2' && ['fast', 'pro'].includes(String(firstCommandArg || '').to
769
766
  const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
770
767
  'activate', '_activate', 'agent', 'chat', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
771
768
  'clean', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync',
772
- 'ingest', 'query', 'lint', 'loop', 'task', 'mission', 'worktree', 'aeo', 'xp', 'play', 'gm', 'x',
769
+ 'ingest', 'query', 'lint', 'loop', 'task', 'mission', 'worktree', 'aeo', 'improve', 'xp', 'play', 'gm', 'x',
773
770
  'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
774
771
  'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet'];
775
772
 
@@ -1045,7 +1042,6 @@ async function interactiveEntry(userInput) {
1045
1042
  // ASCII Welcome Visualization
1046
1043
  function showWelcomeVisualization() {
1047
1044
  const { getBacklogTasks, getInProgressTasks } = require('../lib/state-detection');
1048
- const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
1049
1045
  const cwd = process.cwd();
1050
1046
  const atrisDir = path.join(cwd, 'atris');
1051
1047
  const projectName = path.basename(cwd);
@@ -1059,16 +1055,6 @@ function showWelcomeVisualization() {
1059
1055
  let isInitialized = fs.existsSync(atrisDir);
1060
1056
 
1061
1057
  if (isInitialized) {
1062
- // Auto-create today's journal if missing
1063
- try {
1064
- ensureLogDirectory();
1065
- const { logFile, dateFormatted } = getLogPath();
1066
- if (!fs.existsSync(logFile)) {
1067
- createLogFile(logFile, dateFormatted);
1068
- }
1069
- } catch {
1070
- // Silently fail - don't block welcome display
1071
- }
1072
1058
  // Check MAP.md
1073
1059
  const mapPath = path.join(atrisDir, 'MAP.md');
1074
1060
  if (fs.existsSync(mapPath)) {
@@ -1197,6 +1183,11 @@ if (command === 'init') {
1197
1183
  Promise.resolve(require('../commands/aeo').run(process.argv.slice(3)))
1198
1184
  .then(() => process.exit(0))
1199
1185
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1186
+ } else if (command === 'improve') {
1187
+ // Improve: one paid RL tick via POST /api/improve (deducts credits), local autopilot fallback.
1188
+ Promise.resolve(require('../commands/improve').run(process.argv.slice(3)))
1189
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
1190
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1200
1191
  } else if (command === 'brain') {
1201
1192
  Promise.resolve()
1202
1193
  .then(() => require('../commands/brain').brainCommand(process.argv.slice(3)))
@@ -1567,7 +1558,7 @@ if (command === 'init') {
1567
1558
  searchJournal(keyword);
1568
1559
  } else if (command === 'xp') {
1569
1560
  require('../commands/xp').xpCommand(...process.argv.slice(3))
1570
- .then(() => process.exit(0))
1561
+ .then(() => { process.exitCode = 0; })
1571
1562
  .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
1572
1563
  } else if (command === 'play') {
1573
1564
  require('../commands/play').playCommand(...process.argv.slice(3))
@@ -1622,6 +1613,14 @@ if (command === 'init') {
1622
1613
  integrationsStatus()
1623
1614
  .then(() => process.exit(0))
1624
1615
  .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
1616
+ } else if (command === 'apps') {
1617
+ // Keep APP.md app-pack operations independent from the heavier workspace boot
1618
+ // path so `atris apps --json` stays machine-readable for agents.
1619
+ const subcommand = process.argv[3];
1620
+ const args = process.argv.slice(4);
1621
+ Promise.resolve(require('../commands/apps').appsCommand(subcommand, ...args))
1622
+ .then(() => process.exit(0))
1623
+ .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
1625
1624
  } else if (command === 'learn') {
1626
1625
  const subcommand = process.argv[3];
1627
1626
  const args = process.argv.slice(4);
@@ -1637,7 +1636,9 @@ if (command === 'init') {
1637
1636
  } else if (command === 'member') {
1638
1637
  const subcommand = process.argv[3];
1639
1638
  const args = process.argv.slice(4);
1640
- require('../commands/member').memberCommand(subcommand, ...args);
1639
+ Promise.resolve(require('../commands/member').memberCommand(subcommand, ...args))
1640
+ .then(() => process.exit(0))
1641
+ .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
1641
1642
  } else if (command === 'app') {
1642
1643
  const subcommand = process.argv[3];
1643
1644
  const args = process.argv.slice(4);
package/commands/align.js CHANGED
@@ -173,20 +173,6 @@ async function walkCloud(token, businessId, workspaceId) {
173
173
  return { files: out, errors };
174
174
  }
175
175
 
176
- /**
177
- * Get cloud file content (for hash computation when /files doesn't return hashes).
178
- */
179
- async function fetchCloudFileHash(token, businessId, workspaceId, filePath) {
180
- const result = await apiRequestJson(
181
- `/business/${businessId}/workspaces/${workspaceId}/file?path=${encodeURIComponent(filePath)}`,
182
- { method: 'GET', token }
183
- );
184
- if (!result.ok) return null;
185
- const content = result.data && result.data.content;
186
- if (typeof content !== 'string') return null;
187
- return hashContent(Buffer.from(content, 'utf-8'));
188
- }
189
-
190
176
  /**
191
177
  * Wake the EC2 computer and wait until it's running.
192
178
  * Returns the endpoint URL or null on timeout.
package/commands/apps.js CHANGED
@@ -1,6 +1,10 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { spawnSync } = require('child_process');
4
+ const { ensureValidCredentials } = require('../utils/auth');
5
+ const { apiRequestJson } = require('../utils/api');
6
+
7
+ const CLOUD_LOAD_COMMANDS = new Set(['load', 'cloud', 'mine']);
4
8
 
5
9
  function findAppsPackRoot(startDir = process.cwd()) {
6
10
  const explicit = process.env.ATRIS_APPS_PACK;
@@ -25,6 +29,7 @@ function appsUsageLines() {
25
29
  '',
26
30
  'Commands:',
27
31
  ' list [--json] List available local apps',
32
+ ' load [--filter kind] [--json] Load owned cloud apps from Atris',
28
33
  ' run <slug> [--lines N] Run an app and print data/latest.md or --json status',
29
34
  ' owner <slug> [--json] Show owner view: launch, usage, learning, next actions',
30
35
  ' status [--json] Show local app health',
@@ -156,7 +161,99 @@ function runPackScript(packRoot, script, args) {
156
161
  process.exit(result.status ?? 0);
157
162
  }
158
163
 
159
- function appsCommand(subcommand, ...rawArgs) {
164
+ function normalizeCloudAppsPayload(data) {
165
+ if (Array.isArray(data)) return data;
166
+ if (Array.isArray(data?.apps)) return data.apps;
167
+ if (Array.isArray(data?.items)) return data.items;
168
+ if (Array.isArray(data?.data)) return data.data;
169
+ return [];
170
+ }
171
+
172
+ function compactCloudApp(app) {
173
+ const slug = app?.slug || app?.id || app?.name || 'unknown';
174
+ return {
175
+ id: app?.id || null,
176
+ name: app?.name || slug,
177
+ slug,
178
+ description: app?.description || null,
179
+ template: app?.template || app?.template_slug || null,
180
+ status: app?.status || app?.health || null,
181
+ last_run: app?.last_run || app?.lastRun || app?.last_run_at || null,
182
+ next_run: app?.next_run || app?.nextRun || app?.next_run_at || null,
183
+ };
184
+ }
185
+
186
+ function printCloudApps(apps, filter) {
187
+ const suffix = filter ? ` (${filter})` : '';
188
+ if (apps.length === 0) {
189
+ console.log(`No cloud apps found${suffix}.`);
190
+ return;
191
+ }
192
+ console.log(`Cloud apps${suffix}:`);
193
+ for (const app of apps) {
194
+ const name = String(app.name || app.slug || 'Untitled app');
195
+ const slug = String(app.slug || app.id || 'unknown');
196
+ const status = app.status ? ` status=${app.status}` : '';
197
+ const template = app.template ? ` template=${app.template}` : '';
198
+ console.log(` ${slug.padEnd(24)} ${name}${status}${template}`);
199
+ }
200
+ }
201
+
202
+ function printAppsLoadHelp() {
203
+ console.log('');
204
+ console.log('Usage: atris apps load [--filter <template|paid|free>] [--json]');
205
+ console.log('');
206
+ console.log('Load owned cloud apps from Atris. Requires `atris login`.');
207
+ console.log('');
208
+ }
209
+
210
+ async function loadCloudApps(rawArgs) {
211
+ const args = [...rawArgs];
212
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
213
+ printAppsLoadHelp();
214
+ return;
215
+ }
216
+ const json = args.includes('--json');
217
+ if (json) args.splice(args.indexOf('--json'), 1);
218
+ let filter = popOption(args, '--filter', null);
219
+ if (!filter && args[0] && !args[0].startsWith('-')) filter = args.shift();
220
+ if (args.length > 0) {
221
+ exitAppsError(`Unknown apps load option: ${args[0]}`, json, { usage: true, code: 2 });
222
+ }
223
+
224
+ const ensured = await ensureValidCredentials(apiRequestJson);
225
+ if (ensured.error || !ensured.credentials?.token) {
226
+ exitAppsError('Not logged in. Run: atris login', json, {
227
+ extra: { login: 'atris login' },
228
+ });
229
+ }
230
+
231
+ const query = filter ? `?filter=${encodeURIComponent(filter)}` : '';
232
+ const result = await apiRequestJson(`/apps${query}`, {
233
+ method: 'GET',
234
+ token: ensured.credentials.token,
235
+ });
236
+ if (!result.ok) {
237
+ exitAppsError(`Failed to load cloud apps: ${result.error || result.status || 'request failed'}`, json, {
238
+ extra: { status: result.status || null },
239
+ });
240
+ }
241
+
242
+ const apps = normalizeCloudAppsPayload(result.data).map(compactCloudApp);
243
+ if (json) {
244
+ console.log(JSON.stringify({
245
+ ok: true,
246
+ source: 'cloud',
247
+ filter: filter || null,
248
+ count: apps.length,
249
+ apps,
250
+ }, null, 2));
251
+ return;
252
+ }
253
+ printCloudApps(apps, filter || null);
254
+ }
255
+
256
+ async function appsCommand(subcommand, ...rawArgs) {
160
257
  const jsonRequested = wantsJson(subcommand, rawArgs);
161
258
  const normalized = normalizeInvocation(subcommand, rawArgs);
162
259
  subcommand = normalized.subcommand;
@@ -166,6 +263,10 @@ function appsCommand(subcommand, ...rawArgs) {
166
263
  printAppsHelp();
167
264
  return;
168
265
  }
266
+ if (CLOUD_LOAD_COMMANDS.has(subcommand)) {
267
+ await loadCloudApps(rawArgs);
268
+ return;
269
+ }
169
270
  const packRoot = findAppsPackRoot();
170
271
  if (!packRoot) {
171
272
  if (jsonRequested) {
@@ -25,6 +25,46 @@ const pkg = require('../package.json');
25
25
 
26
26
  const PHASE_TIMEOUT = 600000; // 10 min per phase
27
27
 
28
+ function looksOwnerClaimed(claimed) {
29
+ const text = String(claimed || '').toLowerCase();
30
+ return /\bkeshav(?:rao)?\b/.test(text) || /\b(owner|human|operator)\b/.test(text);
31
+ }
32
+
33
+ function looksOwnerGatedTitle(title) {
34
+ const text = String(title || '').toLowerCase();
35
+ return (
36
+ /\bowner[- ](?:approval|input|gate|gated)\b/.test(text) ||
37
+ /\bhuman[- ](?:approval|input|gate|gated)\b/.test(text) ||
38
+ /\bmanual send\b/.test(text) ||
39
+ /\broute confirmation\b/.test(text) ||
40
+ /\bconfirm pallet destination\b/.test(text) ||
41
+ /\bconfirm .+ destination before .+ approval\b/.test(text) ||
42
+ /\bapprove and manually send\b/.test(text)
43
+ );
44
+ }
45
+
46
+ function shouldSkipAutoHumanGate(task) {
47
+ if (!task) return false;
48
+ return looksOwnerClaimed(task.claimed) || looksOwnerGatedTitle(task.title || task.task);
49
+ }
50
+
51
+ function repoMapAuditReportsClean(cwd) {
52
+ const auditPath = path.join(cwd, 'scripts', 'audit_map_refs.py');
53
+ if (!fs.existsSync(auditPath)) return false;
54
+
55
+ const result = spawnSync('python3', [auditPath], {
56
+ cwd,
57
+ encoding: 'utf8',
58
+ timeout: 120000,
59
+ maxBuffer: 1024 * 1024
60
+ });
61
+ if (result.status !== 0) return false;
62
+
63
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
64
+ const match = output.match(/Total broken references:\s*(\d+)/i);
65
+ return Boolean(match && Number(match[1]) === 0);
66
+ }
67
+
28
68
  /**
29
69
  * Scan workspace for the next thing worth doing.
30
70
  * Returns { task, why, kind } or null.
@@ -54,7 +94,7 @@ async function suggestNextTask(cwd, skipped = new Set(), { auto = false } = {})
54
94
  // --- Resume interrupted work ---
55
95
  if (todo.inProgress.length > 0) {
56
96
  const t = todo.inProgress[0];
57
- if (!(t.tags && t.tags.includes('unverified')) && !skipped.has(t.title)) {
97
+ if (!(t.tags && t.tags.includes('unverified')) && !skipped.has(t.title) && !(auto && shouldSkipAutoHumanGate(t))) {
58
98
  suggestions.push({
59
99
  task: t.title,
60
100
  why: `This was already started${t.claimed ? ` by ${t.claimed}` : ''} but never finished.`,
@@ -75,6 +115,7 @@ async function suggestNextTask(cwd, skipped = new Set(), { auto = false } = {})
75
115
  why: `"${sp.staleSource}" changed on ${sp.sourceDate} but the page was last compiled ${sp.compiledDate}. The content may be wrong.`,
76
116
  kind: 'staleness',
77
117
  priority: 2,
118
+ files: [pageName, sp.staleSource],
78
119
  skipKey: key
79
120
  });
80
121
  break;
@@ -95,7 +136,9 @@ async function suggestNextTask(cwd, skipped = new Set(), { auto = false } = {})
95
136
  }
96
137
 
97
138
  // --- Broken MAP.md references ---
98
- const { unhealable } = healBrokenMapRefs(cwd, atrisDir, true); // dry-run
139
+ const { unhealable } = repoMapAuditReportsClean(cwd)
140
+ ? { unhealable: [] }
141
+ : healBrokenMapRefs(cwd, atrisDir, true); // dry-run
99
142
  if (unhealable.length > 0 && !skipped.has('fix-map-refs')) {
100
143
  const sample = unhealable.slice(0, 3).map(r => `${r.file}:${r.line}`).join(', ');
101
144
  suggestions.push({
@@ -127,6 +170,7 @@ async function suggestNextTask(cwd, skipped = new Set(), { auto = false } = {})
127
170
  for (const t of todo.backlog) {
128
171
  if (t.tags && t.tags.includes('unverified')) continue;
129
172
  if (shouldSkipEndgameAtPicker(cwd, t)) continue;
173
+ if (auto && shouldSkipAutoHumanGate(t)) continue;
130
174
  if (skipped.has(t.title)) continue;
131
175
  const remaining = todo.backlog.filter(b => !(b.tags && b.tags.includes('unverified'))).length;
132
176
  suggestions.push({
@@ -383,10 +427,6 @@ function executePhaseDetailed(phase, context, options = {}) {
383
427
  }
384
428
  }
385
429
 
386
- function executePhase(phase, context, options = {}) {
387
- return executePhaseDetailed(phase, context, options).output;
388
- }
389
-
390
430
  /**
391
431
  * Build context-aware file list for prompts.
392
432
  */
@@ -422,7 +462,13 @@ function buildPrompt(phase, context, options = {}) {
422
462
  contextNote = '',
423
463
  runnerName = '',
424
464
  } = options;
425
- const readFiles = getContextFiles(phase, options);
465
+ const readFiles = getContextFiles(phase, {
466
+ ...options,
467
+ extraReadFiles: [
468
+ ...(options.extraReadFiles || []),
469
+ ...(Array.isArray(context.files) ? context.files : []),
470
+ ],
471
+ });
426
472
  const benchmarkProtocol = benchmarkStrategy === 'stack'
427
473
  ? 'coordinated stack run'
428
474
  : (benchmarkStrategy === 'single' ? 'pinned single-model baseline run' : '');
@@ -478,12 +524,24 @@ When done, reply: done.`;
478
524
  }
479
525
 
480
526
  if (kind === 'staleness' || kind === 'docs' || kind === 'review') {
527
+ const fileList = Array.isArray(context.files) && context.files.length
528
+ ? context.files.map((file) => `- ${file}`).join('\n')
529
+ : '- target page or MAP entry from the task title\n- source file(s) that changed';
481
530
  return `${baseRules}
482
531
 
483
532
  Maintenance task: ${task}
484
533
 
485
- Figure out what needs to change and why. Create focused tasks in atris/TODO.md.
534
+ Relevant files:
535
+ ${fileList}
536
+
537
+ Figure out what needs to change and why. Create exactly one focused task in atris/TODO.md unless the drift truly requires separate commits.
486
538
  For stale pages, read both the page and its sources to understand the drift.
539
+ The task row must include these fields so plan-review can prove it is executable:
540
+ - **Files:** concrete target page plus source file paths
541
+ - **Exit:** the observable post-update state
542
+ - **Verify:** one raw shell command that checks concrete facts and rejects stale phrases; use shell operators like \`&&\`, \`grep -q\`, or \`test\`, not Markdown backticks or English like "returns 1" / "shows today's date"
543
+ - **Rollback:** git checkout -- <changed-files> before commit, or git revert HEAD --no-edit after commit
544
+ Do not write tasks without Verify and Rollback. Do not use \`true\`, \`echo ok\`, or vague "review manually" verification.
487
545
 
488
546
  When done, reply: done.`;
489
547
  }
@@ -802,6 +860,56 @@ function getVerifyCommand(cwd, taskTitle) {
802
860
  return { cmd: detectDefaultVerify(cwd), explicit: false };
803
861
  }
804
862
 
863
+ function collectExplicitVerifyTasks(cwd) {
864
+ const todoPath = path.join(cwd, 'atris', 'TODO.md');
865
+ if (!fs.existsSync(todoPath)) return [];
866
+ const todo = parseTodo(todoPath);
867
+ return [...todo.inProgress, ...(todo.review || []), ...todo.backlog, ...todo.completed]
868
+ .filter((task) => task && task.verify)
869
+ .map((task) => ({
870
+ title: task.title,
871
+ verify: task.verify,
872
+ key: `${task.title}\0${task.verify}`,
873
+ }));
874
+ }
875
+
876
+ function findNewExplicitVerifyCommand(cwd, beforeKeys) {
877
+ const prior = beforeKeys instanceof Set ? beforeKeys : new Set(beforeKeys || []);
878
+ const added = collectExplicitVerifyTasks(cwd).filter((task) => !prior.has(task.key));
879
+ if (added.length !== 1) return null;
880
+ return { cmd: added[0].verify, explicit: true, task: added[0].title };
881
+ }
882
+
883
+ function shouldAdoptPlannedVerify(kind) {
884
+ return ['staleness', 'docs', 'review', 'inbox', 'cleanup', 'feature', 'lessons', 'imagined'].includes(kind);
885
+ }
886
+
887
+ function validateVerifyCommandShape(cmd) {
888
+ const text = String(cmd || '').trim();
889
+ if (!text) return { ok: true };
890
+ if (text.includes('`')) {
891
+ return { ok: false, reason: 'Verify contains markdown backticks instead of a raw shell command' };
892
+ }
893
+ if (/\b(returns?|shows?|equals?|should|must)\b/i.test(text)) {
894
+ return { ok: false, reason: 'Verify contains prose expectations instead of shell operators/assertions' };
895
+ }
896
+ return { ok: true };
897
+ }
898
+
899
+ function haltInvalidVerify(cwd, context, verifyCmd, reason, startedAt, phaseResults = {}) {
900
+ writeLesson(cwd, 'verify-not-runnable', 'fail',
901
+ `Verify \`${verifyCmd}\` for "${context.task}" is not a runnable shell command: ${reason}. Tick halted.`);
902
+ return {
903
+ outcome: 'halted',
904
+ reason: 'verify-not-runnable',
905
+ phaseResults,
906
+ elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
907
+ verifyRan: false,
908
+ verifyPass: false,
909
+ verifyCmd,
910
+ };
911
+ }
912
+
805
913
  /**
806
914
  * Infer a default verify command from the repo shape. Order matters:
807
915
  * package.json with a non-stub test script → `npm test`; then pytest/python;
@@ -878,7 +986,7 @@ Read from disk:
878
986
  - atris/lessons.md (recent failures — last 20 lines)
879
987
 
880
988
  Decide if the plan is safe to execute. Check:
881
- 1. Verify points at a falsifiable rubric or test (not \`true\`, \`echo ok\`, or similar).
989
+ 1. Verify points at a falsifiable raw shell command or rubric (not \`true\`, \`echo ok\`, Markdown backticks, or English like "returns 1" / "shows today's date").
882
990
  Prefer \`atris verify <slug> --section <name>\`.
883
991
  2. Files are explicitly declared (not empty, not vague).
884
992
  3. Rollback is named (commit, checkpoint, or \`git revert\`).
@@ -1170,8 +1278,15 @@ function runTaskOnce(context, options = {}) {
1170
1278
 
1171
1279
  const phaseResults = {};
1172
1280
  const startedAt = Date.now();
1173
- const verifyResult = getVerifyCommand(cwd, context.task);
1174
- const verifyCmd = verifyResult.cmd;
1281
+ let verifyResult = getVerifyCommand(cwd, context.task);
1282
+ let verifyCmd = verifyResult.cmd;
1283
+ const explicitVerifyBefore = new Set(
1284
+ collectExplicitVerifyTasks(cwd).map((task) => task.key)
1285
+ );
1286
+ const initialVerifyShape = validateVerifyCommandShape(verifyCmd);
1287
+ if (!initialVerifyShape.ok) {
1288
+ return haltInvalidVerify(cwd, context, verifyCmd, initialVerifyShape.reason, startedAt, phaseResults);
1289
+ }
1175
1290
 
1176
1291
  // Guard: endgame tasks must have an explicit Verify field.
1177
1292
  // Reactive signals (inbox, staleness, imagined) use npm test as default.
@@ -1264,6 +1379,18 @@ function runTaskOnce(context, options = {}) {
1264
1379
  }
1265
1380
  }
1266
1381
 
1382
+ if (!verifyResult.explicit && shouldAdoptPlannedVerify(context.kind)) {
1383
+ const plannedVerify = findNewExplicitVerifyCommand(cwd, explicitVerifyBefore);
1384
+ if (plannedVerify) {
1385
+ verifyResult = plannedVerify;
1386
+ verifyCmd = plannedVerify.cmd;
1387
+ }
1388
+ }
1389
+ const plannedVerifyShape = validateVerifyCommandShape(verifyCmd);
1390
+ if (!plannedVerifyShape.ok) {
1391
+ return haltInvalidVerify(cwd, context, verifyCmd, plannedVerifyShape.reason, startedAt, phaseResults);
1392
+ }
1393
+
1267
1394
  // Phase: do
1268
1395
  {
1269
1396
  const t0 = Date.now();
@@ -1975,12 +2102,13 @@ function findCodeTodos(cwd) {
1975
2102
  try {
1976
2103
  const out = execFileSync('git', [
1977
2104
  'grep', '-n', '-I', '-E', '(TODO|FIXME)',
1978
- '--', ':!test/', ':!node_modules/', ':!atris/', ':!**/*.md'
2105
+ '--', ':!test/', ':!node_modules/', ':!atris/', ':!**/_archive/**', ':!**/*.md'
1979
2106
  ], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
1980
2107
  const results = [];
1981
2108
  for (const raw of out.split('\n').filter(Boolean)) {
1982
2109
  const m = raw.match(/^([^:]+):(\d+):(.*)$/);
1983
2110
  if (!m) continue;
2111
+ if (m[1].split(/[\\/]/).includes('_archive')) continue;
1984
2112
  const line = m[3];
1985
2113
  // A real TODO is a comment marker at the start of the line (allowing
1986
2114
  // leading indent) followed by TODO/FIXME and at least one word. This
@@ -2160,16 +2288,63 @@ function isLessonResolved(lessonLine, cwd, options = {}) {
2160
2288
  if (!slugMatch) return false;
2161
2289
  const slug = slugMatch[1];
2162
2290
 
2291
+ if (isCleanMapBrokenRefFailLesson(lessonLine, cwd)) return true;
2292
+
2163
2293
  // Detector-backed check (typed lesson sidecar)
2164
2294
  const meta = options.meta || loadLessonMetadata(cwd)[slug];
2165
2295
  if (meta && meta.detector) {
2166
2296
  return runLessonDetector(meta.detector, cwd, options.detectorTimeout);
2167
2297
  }
2168
2298
 
2299
+ if (inlinePythonVerifyFailureNowPasses(lessonLine, cwd, options.detectorTimeout)) return true;
2300
+
2169
2301
  // Legacy fallback: keyword grep against referenced files.
2170
2302
  return isLessonResolvedLegacy(lessonLine, cwd);
2171
2303
  }
2172
2304
 
2305
+ function isCleanMapBrokenRefFailLesson(lessonLine, cwd) {
2306
+ const text = String(lessonLine || '').toLowerCase();
2307
+ if (!/fix \d+ broken references? in map\.md/.test(text)) return false;
2308
+ return repoMapAuditReportsClean(cwd);
2309
+ }
2310
+
2311
+ function extractInlinePythonVerifyFailure(lessonLine) {
2312
+ const commandMatch = String(lessonLine || '').match(/Verify command\s+``([\s\S]*?)``\s+failed/i);
2313
+ if (!commandMatch) return null;
2314
+ const matches = [...commandMatch[1].matchAll(/\b(python3?)\s+-c\s+(["'])([\s\S]*?)\2/g)];
2315
+ const match = matches[matches.length - 1];
2316
+ if (!match) return null;
2317
+ return {
2318
+ executable: match[1],
2319
+ code: match[3].replace(/\\"/g, '"').replace(/\\'/g, "'")
2320
+ };
2321
+ }
2322
+
2323
+ function inlinePythonVerifyFailureNowPasses(lessonLine, cwd, timeout = 10000) {
2324
+ const parsed = extractInlinePythonVerifyFailure(lessonLine);
2325
+ if (!parsed) return false;
2326
+ const result = spawnSync(parsed.executable, ['-c', parsed.code], {
2327
+ cwd,
2328
+ encoding: 'utf8',
2329
+ timeout,
2330
+ stdio: ['ignore', 'ignore', 'ignore']
2331
+ });
2332
+ return result.status === 0;
2333
+ }
2334
+
2335
+ function legacyLessonFileRefs(lessonLine) {
2336
+ const fileRefs = [];
2337
+ const filePattern = /`([a-zA-Z0-9_/./-]+\.[a-zA-Z]+(?::\d+(?:-\d+)?)?)`/g;
2338
+ let m;
2339
+ while ((m = filePattern.exec(lessonLine)) !== null) {
2340
+ const ref = m[1].replace(/:\d+(-\d+)?$/, '');
2341
+ if (ref.includes('/') || ref.endsWith('.js') || ref.endsWith('.md') || ref.endsWith('.ts')) {
2342
+ fileRefs.push(ref);
2343
+ }
2344
+ }
2345
+ return fileRefs;
2346
+ }
2347
+
2173
2348
  /**
2174
2349
  * The pre-v3.8 resolver — kept as an internal fallback for prose-only lessons
2175
2350
  * that don't have detector metadata yet. Never auto-promotes a prose lesson to
@@ -2182,16 +2357,7 @@ function isLessonResolvedLegacy(lessonLine, cwd) {
2182
2357
  if (!slugMatch) return false;
2183
2358
  const slug = slugMatch[1];
2184
2359
 
2185
- // Extract file paths: patterns like `commands/autopilot.js:116` or `commands/run.js:157`
2186
- const fileRefs = [];
2187
- const filePattern = /`([a-zA-Z0-9_/./-]+\.[a-zA-Z]+(?::\d+(?:-\d+)?)?)`/g;
2188
- let m;
2189
- while ((m = filePattern.exec(lessonLine)) !== null) {
2190
- const ref = m[1].replace(/:\d+(-\d+)?$/, ''); // strip line numbers
2191
- if (ref.includes('/') || ref.endsWith('.js') || ref.endsWith('.md') || ref.endsWith('.ts')) {
2192
- fileRefs.push(ref);
2193
- }
2194
- }
2360
+ const fileRefs = legacyLessonFileRefs(lessonLine);
2195
2361
 
2196
2362
  if (fileRefs.length === 0) return false;
2197
2363
 
@@ -2274,6 +2440,9 @@ function pickUnresolvedFailLesson(cwd) {
2274
2440
  const candidates = [];
2275
2441
  for (const lesson of lessons) {
2276
2442
  if (lesson.verdict !== 'fail') continue;
2443
+ if (lesson.id === 'verify-not-falsifiable') continue;
2444
+ if (lesson.id === 'no-verify-field') continue;
2445
+ if (lesson.id === 'verify-failed' && lesson.legacy) continue;
2277
2446
  if (lesson.resolvedTag) continue;
2278
2447
  // Typed lesson with explicit status wins — respect the sidecar.
2279
2448
  // `resolved` = done. `observed` = process rule, not a fixable code state.
@@ -2284,6 +2453,7 @@ function pickUnresolvedFailLesson(cwd) {
2284
2453
  if (s === 'resolved' || s === 'observed') continue;
2285
2454
  if (s === 'attempted' && (lesson.meta.attempts || 0) >= MAX_ATTEMPTS) continue;
2286
2455
  }
2456
+ if (lesson.legacy && legacyLessonFileRefs(lesson.line).length === 0) continue;
2287
2457
  // Detector-backed or legacy grep check.
2288
2458
  if (isLessonResolved(lesson.line, cwd, { meta: lesson.meta })) continue;
2289
2459
 
@@ -2658,6 +2828,7 @@ async function autopilotAtris(description, options = {}) {
2658
2828
  const context = {
2659
2829
  task: suggestion.task,
2660
2830
  kind: suggestion.kind,
2831
+ ...(suggestion.files ? { files: suggestion.files } : {}),
2661
2832
  ...(suggestion.lessonLine ? { lessonLine: suggestion.lessonLine } : {}),
2662
2833
  ...(suggestion.lessonSlug ? { lessonSlug: suggestion.lessonSlug } : {}),
2663
2834
  ...(suggestion.lessonDate ? { lessonDate: suggestion.lessonDate } : {})
@@ -3052,11 +3223,15 @@ module.exports = {
3052
3223
  proposeCandidateHorizons,
3053
3224
  recordTickCommit,
3054
3225
  regressionCheck,
3226
+ repoMapAuditReportsClean,
3227
+ isCleanMapBrokenRefFailLesson,
3228
+ inlinePythonVerifyFailureNowPasses,
3055
3229
  runPlanReview,
3056
3230
  runTaskOnce,
3057
3231
  buildPlanReviewPrompt,
3058
3232
  parseVerdict,
3059
3233
  scoreEndgameCandidates,
3060
3234
  suggestNextTask,
3235
+ shouldSkipAutoHumanGate,
3061
3236
  writeLesson
3062
3237
  };