atris 3.30.8 → 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 (59) hide show
  1. package/AGENTS.md +16 -3
  2. package/FOR_AGENTS.md +81 -0
  3. package/README.md +4 -2
  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 +367 -93
  11. package/bin/atris.js +317 -155
  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 +3659 -282
  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/mission-runtime-loop.js +320 -0
  40. package/lib/next-moves.js +212 -6
  41. package/lib/pulse.js +74 -1
  42. package/lib/runner-command.js +20 -8
  43. package/lib/runs-prune.js +242 -0
  44. package/lib/task-proof.js +1 -1
  45. package/package.json +3 -3
  46. package/decks/atris-seed-pitch-v3.json +0 -118
  47. package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
  48. package/decks/atris-seed-pitch-v5.json +0 -109
  49. package/decks/atris-seed-pitch-v6.json +0 -137
  50. package/decks/atris-seed-pitch-v7.json +0 -133
  51. package/decks/mark-pincus-narrative.json +0 -102
  52. package/decks/mark-pincus-sourcery.json +0 -94
  53. package/decks/yash-applied-compute-detailed.json +0 -150
  54. package/decks/yash-applied-compute-generalist.json +0 -82
  55. package/decks/yash-applied-compute-narrative.json +0 -54
  56. package/lib/ax-chat-input.js +0 -164
  57. package/lib/ax-goal.js +0 -307
  58. package/lib/ax-prefs.js +0 -63
  59. package/lib/ax-shimmer.js +0 -63
@@ -32,7 +32,50 @@ function sqlString(value) {
32
32
  }
33
33
 
34
34
  function resolveStatePath(args = []) {
35
- return path.resolve(expandHome(readFlag(args, '--state', process.env.CODEX_STATE_DB || path.join(os.homedir(), '.codex', 'state_5.sqlite'))));
35
+ const explicit = readFlag(args, '--state', process.env.CODEX_STATE_DB || '');
36
+ if (explicit) return path.resolve(expandHome(explicit));
37
+ // Codex moved native goals into ~/.codex/goals_1.sqlite; older builds kept them in state_5.sqlite.
38
+ // Prefer the live goals DB so the bridge sees real goal activity, fall back to the legacy state DB.
39
+ const goalsDb = path.join(os.homedir(), '.codex', 'goals_1.sqlite');
40
+ const legacyDb = path.join(os.homedir(), '.codex', 'state_5.sqlite');
41
+ return path.resolve(fs.existsSync(goalsDb) ? goalsDb : legacyDb);
42
+ }
43
+
44
+ // Thread metadata (cwd/title) stayed in state_5.sqlite even after goals moved to goals_1.sqlite.
45
+ function resolveThreadsPath(args = []) {
46
+ const explicit = readFlag(args, '--threads-db', process.env.CODEX_THREADS_DB || '');
47
+ if (explicit) return path.resolve(expandHome(explicit));
48
+ const legacyDb = path.join(os.homedir(), '.codex', 'state_5.sqlite');
49
+ return fs.existsSync(legacyDb) ? path.resolve(legacyDb) : '';
50
+ }
51
+
52
+ function tableExists(dbPath, table) {
53
+ if (!dbPath || !fs.existsSync(dbPath)) return false;
54
+ try {
55
+ const rows = runSqliteJson(dbPath, `SELECT name FROM sqlite_master WHERE type='table' AND name=${sqlString(table)} LIMIT 1;`);
56
+ return rows.length > 0;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ // Goals live in goals_1.sqlite (no `threads` table); thread metadata lives in state_5.sqlite.
63
+ // Legacy single-file layouts (and test fixtures) keep both in one DB. Resolve which join to use.
64
+ function goalQueryContext(args = []) {
65
+ const goalsDb = resolveStatePath(args);
66
+ if (tableExists(goalsDb, 'threads')) {
67
+ return { goalsDb, threadsTable: 'threads', prefix: '' };
68
+ }
69
+ const threadsDb = resolveThreadsPath(args);
70
+ if (threadsDb && threadsDb !== goalsDb && tableExists(threadsDb, 'threads')) {
71
+ return { goalsDb, threadsTable: 'tdb.threads', prefix: `ATTACH ${sqlString(threadsDb)} AS tdb;\n` };
72
+ }
73
+ return { goalsDb, threadsTable: null, prefix: '' };
74
+ }
75
+
76
+ function runGoalQuery(args, buildSql) {
77
+ const ctx = goalQueryContext(args);
78
+ return runSqliteJson(ctx.goalsDb, ctx.prefix + buildSql(ctx.threadsTable));
36
79
  }
37
80
 
38
81
  function defaultRunsDir(args = []) {
@@ -81,7 +124,11 @@ function ensureStateDb(dbPath) {
81
124
  }
82
125
  }
83
126
 
84
- function selectGoalSql(whereClause, limit = 10) {
127
+ function selectGoalSql(whereClause, limit = 10, threadsTable = 'threads') {
128
+ const join = threadsTable ? `LEFT JOIN ${threadsTable} t ON t.id = tg.thread_id` : '';
129
+ const cwd = threadsTable ? 't.cwd' : 'NULL';
130
+ const title = threadsTable ? 't.title' : 'NULL';
131
+ const threadUpdated = threadsTable ? 't.updated_at_ms' : 'NULL';
85
132
  return `
86
133
  SELECT
87
134
  tg.thread_id,
@@ -93,41 +140,43 @@ SELECT
93
140
  tg.time_used_seconds,
94
141
  tg.created_at_ms,
95
142
  tg.updated_at_ms,
96
- t.cwd AS thread_cwd,
97
- t.title AS thread_title,
98
- t.updated_at_ms AS thread_updated_at_ms
143
+ ${cwd} AS thread_cwd,
144
+ ${title} AS thread_title,
145
+ ${threadUpdated} AS thread_updated_at_ms
99
146
  FROM thread_goals tg
100
- LEFT JOIN threads t ON t.id = tg.thread_id
147
+ ${join}
101
148
  ${whereClause}
102
- ORDER BY COALESCE(t.updated_at_ms, tg.updated_at_ms) DESC
149
+ ORDER BY COALESCE(${threadUpdated}, tg.updated_at_ms) DESC
103
150
  LIMIT ${Number(limit) || 10}
104
151
  `;
105
152
  }
106
153
 
107
- function readGoalByThread(dbPath, threadId) {
108
- const rows = runSqliteJson(dbPath, selectGoalSql(`WHERE tg.thread_id = ${sqlString(threadId)}`, 1));
154
+ function readGoalByThread(args, threadId) {
155
+ const rows = runGoalQuery(args, (tt) => selectGoalSql(`WHERE tg.thread_id = ${sqlString(threadId)}`, 1, tt));
109
156
  return rows[0] || null;
110
157
  }
111
158
 
112
- function readLatestGoalForCwd(dbPath, cwd) {
159
+ function readLatestGoalForCwd(args, cwd) {
160
+ const ctx = goalQueryContext(args);
161
+ if (!ctx.threadsTable) return null; // cannot cwd-match without thread metadata
113
162
  const realCwd = fs.realpathSync.native ? fs.realpathSync.native(cwd) : fs.realpathSync(cwd);
114
163
  const pwd = process.env.PWD || '';
115
164
  const pwdReal = pwd && fs.existsSync(pwd) ? (fs.realpathSync.native ? fs.realpathSync.native(pwd) : fs.realpathSync(pwd)) : '';
116
165
  const candidates = [...new Set([cwd, realCwd, pwdReal === realCwd ? pwd : ''].filter(Boolean))];
117
- const rows = runSqliteJson(dbPath, selectGoalSql(`WHERE t.cwd IN (${candidates.map(sqlString).join(', ')})`, 1));
166
+ const rows = runSqliteJson(ctx.goalsDb, ctx.prefix + selectGoalSql(`WHERE t.cwd IN (${candidates.map(sqlString).join(', ')})`, 1, ctx.threadsTable));
118
167
  return rows[0] || null;
119
168
  }
120
169
 
121
- function readRecentGoals(dbPath, limit = 10) {
122
- return runSqliteJson(dbPath, selectGoalSql('', limit));
170
+ function readRecentGoals(args, limit = 10) {
171
+ return runGoalQuery(args, (tt) => selectGoalSql('', limit, tt));
123
172
  }
124
173
 
125
- function resolveThreadGoal(dbPath, args) {
174
+ function resolveThreadGoal(args) {
126
175
  const explicitThread = readFlag(args, '--thread', '');
127
- if (explicitThread) return readGoalByThread(dbPath, explicitThread);
128
- if (hasFlag(args, '--latest')) return readLatestGoalForCwd(dbPath, process.cwd());
176
+ if (explicitThread) return readGoalByThread(args, explicitThread);
177
+ if (hasFlag(args, '--latest')) return readLatestGoalForCwd(args, process.cwd());
129
178
  const envThread = process.env.CODEX_THREAD_ID || '';
130
- if (envThread) return readGoalByThread(dbPath, envThread);
179
+ if (envThread) return readGoalByThread(args, envThread);
131
180
  return null;
132
181
  }
133
182
 
@@ -163,7 +212,7 @@ function statusCommand(args) {
163
212
  const dbPath = resolveStatePath(args);
164
213
  ensureStateDb(dbPath);
165
214
 
166
- const goal = resolveThreadGoal(dbPath, args);
215
+ const goal = resolveThreadGoal(args);
167
216
  if (goal) {
168
217
  const payload = { ok: true, schema: SCHEMA, action: 'status', state_path: dbPath, goal };
169
218
  printJsonOrText(payload, [
@@ -176,7 +225,7 @@ function statusCommand(args) {
176
225
  }
177
226
 
178
227
  const limit = Math.max(1, Math.min(50, Number(readFlag(args, '--limit', '10')) || 10));
179
- const goals = readRecentGoals(dbPath, limit);
228
+ const goals = readRecentGoals(args, limit);
180
229
  const payload = { ok: true, schema: SCHEMA, action: 'status', state_path: dbPath, goals };
181
230
  printJsonOrText(payload, [
182
231
  `Codex goals: ${goals.length} recent`,
@@ -192,7 +241,7 @@ function resetCommand(args) {
192
241
  ensureStateDb(dbPath);
193
242
 
194
243
  const startedAt = new Date().toISOString();
195
- const goal = resolveThreadGoal(dbPath, args);
244
+ const goal = resolveThreadGoal(args);
196
245
  if (!goal) {
197
246
  throw new Error('No Codex goal found. Pass --thread <thread-id> or --latest.');
198
247
  }
@@ -238,7 +287,7 @@ SELECT changes() AS deleted;
238
287
  COMMIT;
239
288
  `, { readonly: false });
240
289
  const deleted = Number(rows[0]?.deleted || 0);
241
- const remaining = readGoalByThread(dbPath, goal.thread_id);
290
+ const remaining = readGoalByThread(args, goal.thread_id);
242
291
  const ok = deleted === 1 && !remaining;
243
292
  const payload = writeReceipt(outDir, {
244
293
  ok,
@@ -277,7 +326,8 @@ function usage() {
277
326
  ` atris codex-goal reset --thread <id> ${CONFIRM_RESET_FLAG}`,
278
327
  '',
279
328
  'Flags:',
280
- ' --state <path> Codex state DB (default ~/.codex/state_5.sqlite)',
329
+ ' --state <path> Codex goals DB (default ~/.codex/goals_1.sqlite, falls back to state_5.sqlite)',
330
+ ' --threads-db <path> Codex thread metadata DB for cwd/title (default ~/.codex/state_5.sqlite)',
281
331
  ' --latest Use the latest Codex goal whose thread cwd matches the current directory',
282
332
  ' --out-dir <path> Receipt/backup directory (default .atris/runs)',
283
333
  '',
@@ -1635,6 +1635,41 @@ async function resolveBusinessContextBySlug(token, slug, options = {}) {
1635
1635
  return null;
1636
1636
  }
1637
1637
 
1638
+ function shortcutBusinessLabel(slug, fallbackName) {
1639
+ if (normalizeBusinessSlug(slug) === RECRUITING_BUSINESS_SLUG) return 'Atris Labs';
1640
+ return fallbackName || slug;
1641
+ }
1642
+
1643
+ async function resolveSingleBusinessShortcutFallback(token, slug) {
1644
+ const wantedSlug = normalizeBusinessSlug(slug);
1645
+ if (wantedSlug !== RECRUITING_BUSINESS_SLUG) return null;
1646
+
1647
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
1648
+ const rows = list.ok && Array.isArray(list.data)
1649
+ ? list.data.filter((business) => business && business.id)
1650
+ : [];
1651
+ if (rows.length !== 1) return null;
1652
+
1653
+ const match = rows[0];
1654
+ const businesses = loadBusinesses();
1655
+ const businessName = shortcutBusinessLabel(wantedSlug, match.name || match.slug);
1656
+ businesses[wantedSlug] = {
1657
+ business_id: match.id,
1658
+ workspace_id: match.workspace_id,
1659
+ name: businessName,
1660
+ slug: wantedSlug,
1661
+ cloud_slug: match.slug || null,
1662
+ added_at: new Date().toISOString(),
1663
+ };
1664
+ saveBusinesses(businesses);
1665
+ return {
1666
+ slug: wantedSlug,
1667
+ businessId: match.id,
1668
+ workspaceId: match.workspace_id,
1669
+ businessName,
1670
+ };
1671
+ }
1672
+
1638
1673
  async function resolveComputerCommandContext(token, options = {}) {
1639
1674
  if (options.businessSlug || options.workspaceId) {
1640
1675
  const ctx = options.businessSlug
@@ -1660,7 +1695,10 @@ async function resolveTypedBusinessComputerContext(token, options = {}, defaults
1660
1695
  return resolveComputerCommandContext(token, { ...options, businessSlug });
1661
1696
  }
1662
1697
 
1663
- const ctx = await resolveBusinessContextBySlug(token, businessSlug, { preferCache: true });
1698
+ let ctx = await resolveBusinessContextBySlug(token, businessSlug, { preferCache: true });
1699
+ if (!ctx?.businessId && (options.allowCanonicalShortcutFallback || defaults.allowCanonicalShortcutFallback)) {
1700
+ ctx = await resolveSingleBusinessShortcutFallback(token, businessSlug);
1701
+ }
1664
1702
  if (!ctx?.businessId) return null;
1665
1703
  const workspaces = await listBusinessWorkspaces(token, ctx);
1666
1704
  const workspace = resolveWorkspaceByComputerType(workspaces, computerType);
@@ -1705,7 +1743,7 @@ async function resolveWorkspaceSelector(token, ctx, input) {
1705
1743
  return workspace?.id || selector;
1706
1744
  }
1707
1745
 
1708
- async function resolveBusinessOwnerForCreate(token, businessSlug = null) {
1746
+ async function resolveBusinessOwnerForCreate(token, businessSlug = null, options = {}) {
1709
1747
  const wantedSlug = businessSlug ? String(businessSlug).trim() : null;
1710
1748
  if (wantedSlug) {
1711
1749
  const fromApi = await resolveBusinessContextBySlug(token, wantedSlug);
@@ -1720,6 +1758,9 @@ async function resolveBusinessOwnerForCreate(token, businessSlug = null) {
1720
1758
  businessName: cached.name || cached.slug || wantedSlug,
1721
1759
  };
1722
1760
  }
1761
+ if (options.allowCanonicalShortcutFallback) {
1762
+ return resolveSingleBusinessShortcutFallback(token, wantedSlug);
1763
+ }
1723
1764
  return null;
1724
1765
  }
1725
1766
 
@@ -2248,7 +2289,9 @@ async function computerCreate(token, args = [], defaults = {}) {
2248
2289
  return;
2249
2290
  }
2250
2291
 
2251
- const ctx = await resolveBusinessOwnerForCreate(token, options.businessSlug);
2292
+ const ctx = await resolveBusinessOwnerForCreate(token, options.businessSlug, {
2293
+ allowCanonicalShortcutFallback: Boolean(defaults.allowCanonicalShortcutFallback),
2294
+ });
2252
2295
  if (!ctx?.businessId) {
2253
2296
  console.error('No business found.');
2254
2297
  console.error('Run inside a bound business workspace or pass: --business <slug>');
@@ -3874,6 +3917,7 @@ async function runRecruitingComputerShortcut(token, args, cloudOptions = {}) {
3874
3917
  return computerCreate(token, finalArgs, {
3875
3918
  businessSlug: cloudOptions.businessSlug || RECRUITING_BUSINESS_SLUG,
3876
3919
  computerType: 'recruiting',
3920
+ allowCanonicalShortcutFallback: !cloudOptions.businessSlug,
3877
3921
  });
3878
3922
  }
3879
3923
 
@@ -3888,6 +3932,7 @@ async function runRecruitingComputerShortcut(token, args, cloudOptions = {}) {
3888
3932
  const ctx = await resolveTypedBusinessComputerContext(token, recruitingOptions, {
3889
3933
  businessSlug: RECRUITING_BUSINESS_SLUG,
3890
3934
  computerType: 'recruiting',
3935
+ allowCanonicalShortcutFallback: !recruitingOptions.businessSlug,
3891
3936
  });
3892
3937
  if (!ctx?.businessId) {
3893
3938
  console.error('Atris Recruiting is not available for this account.');
package/commands/gm.js CHANGED
@@ -331,7 +331,7 @@ function gmState(args = []) {
331
331
  missions,
332
332
  review_queue: reviewQueue,
333
333
  next_commands: commands,
334
- xp_rule: 'GM can route missions and review proof, but AgentXP still lands only after human accept.',
334
+ xp_rule: 'GM can route missions and review proof, but AgentXP is awarded only after human approval.',
335
335
  global_sync_rule: AGENTXP_GLOBAL_SYNC_RULE,
336
336
  leaderboard_url: AGENTXP_LEADERBOARD_URL,
337
337
  };
@@ -0,0 +1,179 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const {
4
+ isGenericInboxPlaceholder,
5
+ seedInboxFromMove,
6
+ } = require('../lib/next-moves');
7
+
8
+ function hasFlag(args, name) {
9
+ return args.includes(name);
10
+ }
11
+
12
+ function safeRead(file) {
13
+ try { return fs.readFileSync(file, 'utf8'); } catch { return ''; }
14
+ }
15
+
16
+ function walkFiles(dir, predicate, limit = 200) {
17
+ const out = [];
18
+ const visit = (current) => {
19
+ if (out.length >= limit) return;
20
+ let entries = [];
21
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
22
+ for (const entry of entries) {
23
+ if (out.length >= limit) break;
24
+ const full = path.join(current, entry.name);
25
+ if (entry.isDirectory()) visit(full);
26
+ else if (!predicate || predicate(full)) out.push(full);
27
+ }
28
+ };
29
+ visit(dir);
30
+ return out.sort();
31
+ }
32
+
33
+ function clip(value, max = 140) {
34
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
35
+ if (text.length <= max) return text;
36
+ return `${text.slice(0, max - 3).trim()}...`;
37
+ }
38
+
39
+ function addAction(actions, title, source, reason, evidence = null) {
40
+ const clean = clip(title, 180);
41
+ if (!clean || isGenericInboxPlaceholder(clean)) return;
42
+ const key = clean.toLowerCase();
43
+ if (actions.some((action) => action.key === key)) return;
44
+ actions.push({
45
+ key,
46
+ title: clean,
47
+ source,
48
+ reason: clip(reason, 220),
49
+ evidence,
50
+ });
51
+ }
52
+
53
+ function receiptActions(root, actions) {
54
+ const runsDir = path.join(root, 'atris', 'runs');
55
+ for (const file of walkFiles(runsDir, (full) => /\.json$/i.test(full), 300).slice(-80)) {
56
+ let receipt = null;
57
+ try { receipt = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { receipt = null; }
58
+ if (!receipt || receipt.schema !== 'atris.mission_receipt.v1') continue;
59
+ const rel = path.relative(root, file);
60
+ const landing = receipt.result?.landing || {};
61
+ const text = [
62
+ receipt.objective,
63
+ landing.changed,
64
+ landing.reason,
65
+ landing.checked,
66
+ landing.tested,
67
+ landing.next,
68
+ receipt.result?.reason,
69
+ ].filter(Boolean).join(' ');
70
+ if (/no concrete follow-up|stop|placeholder|zero value score|near[- ]?miss/i.test(text)) {
71
+ addAction(actions, 'Improve the next-mission chooser so it explains stopped work and better candidates', 'receipt', text, rel);
72
+ }
73
+ if (receipt.result?.verifier_result?.passed === false || /\b(verifier|test|check).{0,40}\b(fail|failed|error)\b/i.test(text)) {
74
+ addAction(actions, `Fix failing verifier for ${receipt.objective || 'latest mission'}`, 'receipt', text, rel);
75
+ }
76
+ if (landing.reason && landing.proof && /receipt saved/i.test(landing.proof)) {
77
+ addAction(actions, 'Surface buried result landing reasoning in the normal mission view', 'receipt', landing.reason, rel);
78
+ }
79
+ if (landing.proof == null || /proof:\s*null/i.test(text)) {
80
+ addAction(actions, 'Repair proof mismatch where proof exists in metadata but renders as null', 'receipt', text, rel);
81
+ }
82
+ }
83
+ }
84
+
85
+ function runLogActions(root, actions) {
86
+ const logsDir = path.join(root, 'atris', 'logs', 'runs');
87
+ for (const file of walkFiles(logsDir, (full) => /\.(log|txt|md|json)$/i.test(full), 200).slice(-60)) {
88
+ const rel = path.relative(root, file);
89
+ const text = safeRead(file);
90
+ if (!text) continue;
91
+ if (/no concrete follow-up|placeholder|test idea|zero value score/i.test(text)) {
92
+ addAction(actions, 'Harvest weak chooser traces into a concrete next-mission fix', 'runlog', text, rel);
93
+ }
94
+ if (/\b(error|failed|exception|traceback|unhandled)\b/i.test(text)) {
95
+ addAction(actions, 'Turn recent run-log failures into a bounded fix task with proof', 'runlog', text, rel);
96
+ }
97
+ }
98
+ }
99
+
100
+ function thinkingActions(root, actions) {
101
+ const rel = 'atris/thinking.md';
102
+ const text = safeRead(path.join(root, rel));
103
+ if (!text) return;
104
+ if (/plain English|jargon|feynman|understand/i.test(text)) {
105
+ addAction(actions, 'Make proof and task previews plain enough to understand without opening logs', 'thinking', text, rel);
106
+ }
107
+ if (/proof over motion|receipt|verified|checked/i.test(text)) {
108
+ addAction(actions, 'Audit proof surfaces for null proof, hidden receipts, and vague verifier claims', 'thinking', text, rel);
109
+ }
110
+ if (/stop.*no concrete|no concrete.*stop|fake|busywork/i.test(text)) {
111
+ addAction(actions, 'Make autonomous loops stop or ask instead of inventing fake work', 'thinking', text, rel);
112
+ }
113
+ }
114
+
115
+ function harvestActions(root = process.cwd()) {
116
+ const actions = [];
117
+ receiptActions(root, actions);
118
+ runLogActions(root, actions);
119
+ thinkingActions(root, actions);
120
+ return actions.slice(0, 12).map(({ key, ...action }, index) => ({
121
+ id: `I${index + 1}`,
122
+ ...action,
123
+ }));
124
+ }
125
+
126
+ function harvestCommand(args = []) {
127
+ const root = process.cwd();
128
+ if (hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help') {
129
+ console.log('Usage: atris harvest [--write] [--json]');
130
+ console.log('');
131
+ console.log('Find plain next actions from mission receipts, run logs, and thinking.md.');
132
+ console.log('Read-only by default. Use --write to append actions to today\'s Inbox.');
133
+ return { ok: true, action: 'help' };
134
+ }
135
+ const asJson = hasFlag(args, '--json');
136
+ const write = hasFlag(args, '--write');
137
+ const actions = harvestActions(root);
138
+ const writes = [];
139
+ if (write) {
140
+ for (const action of actions) {
141
+ const written = seedInboxFromMove(root, { title: action.title, source: action.source });
142
+ writes.push({
143
+ title: action.title,
144
+ file: written.file ? path.relative(root, written.file) : null,
145
+ line: written.line,
146
+ already_present: Boolean(written.alreadyPresent),
147
+ });
148
+ }
149
+ }
150
+ const payload = {
151
+ ok: true,
152
+ action: 'harvest',
153
+ write,
154
+ count: actions.length,
155
+ actions,
156
+ writes,
157
+ };
158
+ if (asJson) {
159
+ console.log(JSON.stringify(payload, null, 2));
160
+ return payload;
161
+ }
162
+ if (!actions.length) {
163
+ console.log('No harvest actions found.');
164
+ return payload;
165
+ }
166
+ for (const action of actions) {
167
+ console.log(`- **${action.id}:** ${action.title}`);
168
+ console.log(` reason: ${action.reason}`);
169
+ if (action.evidence) console.log(` evidence: ${action.evidence}`);
170
+ }
171
+ if (write) console.log(`Wrote ${writes.filter((item) => item.line).length} inbox action(s).`);
172
+ else console.log('Read-only. Add --write to append these to today\'s Inbox.');
173
+ return payload;
174
+ }
175
+
176
+ module.exports = {
177
+ harvestActions,
178
+ harvestCommand,
179
+ };
package/commands/init.js CHANGED
@@ -512,7 +512,7 @@ function initAtris() {
512
512
  if (!fs.existsSync(featuresDir)) {
513
513
  fs.mkdirSync(featuresDir, { recursive: true });
514
514
  const featuresReadme = path.join(featuresDir, 'README.md');
515
- fs.writeFileSync(featuresReadme, '# Features\n\nThis directory tracks all features built using the atrisDev protocol.\n\nEach feature has:\n- `[feature-name]/idea.md` - Problem, solution, diagrams, success criteria\n- `[feature-name]/build.md` - Implementation plan, files changed, testing\n- `[feature-name]/validate.md` - End-to-end simulation script\n\n---\n\n## Features Built\n\n*Features will appear here as you build them.*\n');
515
+ fs.writeFileSync(featuresReadme, '# Features\n\nThis directory tracks all features built using the Atris protocol.\n\nEach feature has:\n- `[feature-name]/idea.md` - Problem, solution, diagrams, success criteria\n- `[feature-name]/build.md` - Implementation plan, files changed, testing\n- `[feature-name]/validate.md` - End-to-end simulation script\n\n---\n\n## Features Built\n\n*Features will appear here as you build them.*\n');
516
516
  console.log('✓ Created features/ directory with README');
517
517
  }
518
518