atris 3.56.0 → 3.56.2

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 (45) hide show
  1. package/atris/skills/engines/SKILL.md +11 -3
  2. package/atris/skills/x-search/SKILL.md +20 -1
  3. package/atris/skills/youtube/SKILL.md +51 -43
  4. package/bin/atris.js +134 -90
  5. package/commands/auth.js +6 -1
  6. package/commands/autopilot-front.js +29 -13
  7. package/commands/brainstorm.js +77 -476
  8. package/commands/business.js +13 -1
  9. package/commands/engine.js +7 -0
  10. package/commands/experiments.js +178 -0
  11. package/commands/fleet-report.js +2 -2
  12. package/commands/founder.js +12 -0
  13. package/commands/human-missions.js +64 -2
  14. package/commands/init.js +2 -35
  15. package/commands/integrations.js +100 -0
  16. package/commands/land.js +53 -23
  17. package/commands/later.js +52 -0
  18. package/commands/launchpad.js +45 -10
  19. package/commands/log.js +79 -30
  20. package/commands/mission.js +117 -22
  21. package/commands/next.js +153 -63
  22. package/commands/now.js +115 -38
  23. package/commands/recap.js +97 -4
  24. package/commands/run-front.js +20 -5
  25. package/commands/spaceship.js +52 -12
  26. package/commands/status.js +38 -7
  27. package/commands/task.js +56 -19
  28. package/commands/terminal.js +5 -5
  29. package/commands/workflow.js +19 -5
  30. package/commands/x-search.js +123 -13
  31. package/commands/youtube.js +941 -36
  32. package/lib/account-bound.js +7 -0
  33. package/lib/apply-gate.js +102 -0
  34. package/lib/config-guard.js +106 -0
  35. package/lib/context-gatherer.js +55 -8
  36. package/lib/engine-ask.js +16 -6
  37. package/lib/engine-registry.js +14 -4
  38. package/lib/first-minute.js +571 -26
  39. package/lib/known-commands.js +1 -1
  40. package/lib/pack-capabilities.js +13 -3
  41. package/lib/runner-command.js +5 -3
  42. package/lib/scratch-root.js +44 -0
  43. package/lib/workspace-scaffold.js +3 -1
  44. package/package.json +1 -1
  45. package/utils/auth.js +84 -6
package/commands/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
1
+ const { AGENT_TOKEN_EXPIRED_DETAIL, loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
2
2
  const { getAppBaseUrl, apiRequestJson } = require('../utils/api');
3
3
  const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
4
4
  const { hasFlag, readFlag } = require('../lib/arg-parser');
@@ -281,6 +281,11 @@ async function mintAgentToken(args = [], deps = {}) {
281
281
  async function printWhoamiPayload(asJson) {
282
282
  const ensured = await ensureValidCredentials(apiRequestJson);
283
283
  if (ensured.error) {
284
+ if (!asJson && ensured.detail === AGENT_TOKEN_EXPIRED_DETAIL) {
285
+ console.error('the agent key expired.');
286
+ console.error('atris login --agent');
287
+ process.exit(1);
288
+ }
284
289
  if (asJson) {
285
290
  console.log(JSON.stringify({
286
291
  ok: false,
@@ -8,7 +8,10 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { spawn } = require('child_process');
10
10
  const { pickRunnableMission, runBudgetSeconds } = require('./run-front');
11
- const { refuseHeadlessUnless, wantsJson, hasYesFlag } = require('../lib/noninteractive');
11
+ const { wantsJson, hasYesFlag } = require('../lib/noninteractive');
12
+ const { isUnboundScratchFolder, refuseUnboundScratch } = require('../lib/scratch-root');
13
+ const { resolveWorkspaceRoot } = require('../lib/mission-root');
14
+ const { personName } = require('../lib/first-minute');
12
15
 
13
16
  const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
14
17
  const DEFAULT_LEG_WALL_SECONDS = 3600;
@@ -190,13 +193,23 @@ function autopilotStatus(root = process.cwd()) {
190
193
  return 0;
191
194
  }
192
195
 
196
+ function greet(person) {
197
+ return person ? `hey ${person}, ` : '';
198
+ }
199
+
200
+ function inviteLines() {
201
+ return [
202
+ `${greet(personName())}I can keep working until you stop.`,
203
+ '',
204
+ 'next: atris autopilot --yes',
205
+ ];
206
+ }
207
+
193
208
  function showFrontHelp() {
194
209
  console.log('');
195
210
  console.log('Usage: atris autopilot [options]');
196
211
  console.log('');
197
- console.log('Keeps the workspace moving: picks the most logical mission or member,');
198
- console.log('drives it through the mission runtime, then picks the next one.');
199
- console.log('Runs until you stop it.');
212
+ console.log('Keep working until you stop. Pass --yes to start.');
200
213
  console.log('');
201
214
  console.log('Options:');
202
215
  console.log(' --minutes N | --hours N Total budget (default: unlimited)');
@@ -216,7 +229,7 @@ async function autopilotFront(args = []) {
216
229
  // (e.g. backend/) wrote its loop state into a nested .atris, so a later
217
230
  // `autopilot stop`/`status` from the root couldn't see the running loop.
218
231
  // Same resolver the mission/task/usage stores use; falls back to cwd.
219
- const root = require('../lib/mission-root').resolveWorkspaceRoot(process.cwd());
232
+ const root = resolveWorkspaceRoot(process.cwd());
220
233
  if (args[0] === 'stop') return autopilotStop(root);
221
234
  if (args[0] === 'status') return autopilotStatus(root);
222
235
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') { showFrontHelp(); return 0; }
@@ -232,14 +245,16 @@ async function autopilotFront(args = []) {
232
245
  }, null, 2));
233
246
  return 2;
234
247
  }
235
- // Bare headless invoke used to start a mission loop and hang. --auto is
236
- // the existing proceed flag pulse and spaceship already pass. --once is
237
- // a duration, not consent: it still needs --yes off a TTY.
238
- if (!args.includes('--auto') && refuseHeadlessUnless(args, {
239
- allowYes: true,
240
- allowOnce: false,
241
- usage: 'Usage: atris autopilot [--yes|--auto] [--once]',
242
- })) return 2;
248
+ // Bare invoke used to start a loop. --auto is the proceed flag pulse
249
+ // and spaceship already pass. --once is a duration, not consent.
250
+ if (!args.includes('--auto') && !hasYesFlag(args)) {
251
+ for (const line of inviteLines()) console.log(line);
252
+ return 2;
253
+ }
254
+
255
+ // --yes / --auto start the loop. They are not a workspace unlock.
256
+ // An unbound scratch folder is not a room (same class as slack/gmail).
257
+ if (isUnboundScratchFolder(root)) return refuseUnboundScratch();
243
258
 
244
259
  const existing = readState(root);
245
260
  if (existing && pidAlive(existing.pid) && existing.pid !== process.pid) {
@@ -324,6 +339,7 @@ async function autopilotFront(args = []) {
324
339
 
325
340
  module.exports = {
326
341
  autopilotFront,
342
+ inviteLines,
327
343
  maxLegsFlag,
328
344
  stopRequested,
329
345
  requestStop,
@@ -1,504 +1,105 @@
1
+ 'use strict';
2
+
1
3
  const fs = require('fs');
2
4
  const path = require('path');
3
- const readline = require('readline');
4
5
  const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
5
- // Inbox helpers live canonically (and CRLF-tolerant) in lib/file-ops; brainstorm
6
- // used to carry byte-identical local copies that silently missed CRLF journals.
7
- const {
8
- parseInboxItems,
9
- replaceInboxSection,
10
- addInboxItemToContent,
11
- getNextInboxId,
12
- addInboxIdea,
13
- } = require('../lib/file-ops');
14
- const { loadConfig } = require('../utils/config');
15
- const { loadCredentials, ensureValidCredentials } = require('../utils/auth');
16
- const { apiRequestJson } = require('../utils/api');
17
- const { isNonInteractive } = require('../lib/noninteractive');
18
- const { planAtris, doAtris, reviewAtris } = require('./workflow');
19
-
20
- const pkg = require('../package.json');
21
-
22
- async function brainstormAtris() {
23
- const args = process.argv.slice(3);
24
- if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
25
- console.log('');
26
- console.log('Usage: atris brainstorm [idea] [--cloud]');
27
- console.log('');
28
- console.log('Description:');
29
- console.log(' Guided prompt generator for exploration before planning.');
30
- console.log(' Default is local-first; pass --cloud to include AtrisOS journal context.');
31
- console.log(' Headless agents: pass the idea on the command line (never prompts).');
32
- console.log('');
33
- console.log('Options:');
34
- console.log(' --cloud Include AtrisOS journal context (optional).');
35
- console.log(' --no-cloud Force local-only mode (skip AtrisOS).');
36
- console.log(' --yes Non-interactive: capture idea args and exit.');
37
- console.log('');
38
- return;
39
- }
40
-
41
- const targetDir = path.join(process.cwd(), 'atris');
42
- if (!fs.existsSync(targetDir)) {
43
- throw new Error('atris/ folder not found. Run "atris init" first.');
44
- }
45
-
46
- ensureLogDirectory();
47
- const { logFile, dateFormatted } = getLogPath();
48
- if (!fs.existsSync(logFile)) {
49
- createLogFile(logFile, dateFormatted);
50
- }
51
-
52
- const useCloudJournal = args.includes('--cloud') && !args.includes('--no-cloud');
53
- const topicFromArgs = args.filter((arg) => !arg.startsWith('-')).join(' ').trim() || null;
54
-
55
- // Headless agents must never hang on "Describe the desired outcome".
56
- if (isNonInteractive(args)) {
57
- if (topicFromArgs) {
58
- const newId = addInboxIdea(logFile, topicFromArgs);
59
- console.log(`captured I${newId}: ${topicFromArgs}`);
60
- console.log(`journal: ${path.relative(process.cwd(), logFile) || logFile}`);
61
- console.log('Next: atris plan');
62
- return;
63
- }
64
- console.log('brainstorm is interactive without an idea on the command line.');
65
- console.log(`journal: ${path.relative(process.cwd(), logFile) || logFile}`);
66
- console.log('Next: atris brainstorm "<idea>"');
67
- return;
68
- }
6
+ const { addInboxIdea } = require('../lib/file-ops');
7
+ const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
8
+ const { wantsJson } = require('../lib/noninteractive');
9
+ const { compactErrorPayload, compactSuccessPayload, printCliJson } = require('../lib/cli-json');
69
10
 
11
+ function printHelp() {
70
12
  console.log('');
71
- console.log('┌─────────────────────────────────────────────────────────────┐');
72
- console.log('│ Atris Brainstorm — structured prompt generator │');
73
- console.log('└─────────────────────────────────────────────────────────────┘');
13
+ console.log('Usage: atris brainstorm "<idea>" [--json]');
74
14
  console.log('');
75
- console.log(`Date: ${dateFormatted}`);
76
- console.log('Type "exit" at any prompt to cancel.');
15
+ console.log('Description:');
16
+ console.log(' Capture an idea to today\'s inbox and exit.');
17
+ console.log(' Never waits on a prompt. Headless.');
18
+ console.log('');
19
+ console.log('Options:');
20
+ console.log(' --json Print a JSON receipt.');
21
+ console.log(' --cloud Accepted. Capture stays local.');
22
+ console.log(' --no-cloud Force local-only mode.');
23
+ console.log(' --yes Accepted. Always non-interactive.');
77
24
  console.log('');
78
-
79
- // Local journal context (source of truth for Inbox)
80
- let localJournalContext = '';
81
- if (fs.existsSync(logFile)) {
82
- localJournalContext = fs.readFileSync(logFile, 'utf8');
83
- }
84
-
85
- // Optional: fetch journal context from backend (for hints only)
86
- let remoteJournalContext = '';
87
- const config = loadConfig();
88
- const ensured1 = await ensureValidCredentials(apiRequestJson);
89
- const credentials = ensured1.error ? null : ensured1.credentials;
90
-
91
- if (useCloudJournal && config.agent_id && credentials && credentials.token) {
92
- try {
93
- console.log('📖 Fetching latest journal entry from AtrisOS...');
94
- const journalResult = await apiRequestJson(`/agents/${config.agent_id}/journal/today`, {
95
- method: 'GET',
96
- token: credentials.token,
97
- });
98
-
99
- if (journalResult.ok && journalResult.data?.content) {
100
- remoteJournalContext = journalResult.data.content;
101
- console.log('✓ Loaded journal entry from backend');
102
- } else {
103
- // Try fetching latest entry if today doesn't exist
104
- const listResult = await apiRequestJson(`/agents/${config.agent_id}/journal/?limit=1`, {
105
- method: 'GET',
106
- token: credentials.token,
107
- });
108
-
109
- if (listResult.ok && listResult.data?.entries?.length > 0) {
110
- remoteJournalContext = listResult.data.entries[0].content || '';
111
- console.log('✓ Loaded latest journal entry from backend');
112
- }
113
- }
114
- } catch (error) {
115
- // Silently fail - we'll use local log file instead
116
- console.log('ℹ️ Using local journal file (backend unavailable)');
117
- }
118
- console.log('');
119
- }
120
-
121
- // Keep prompts high-signal: only include "recent context" when explicitly pulled from cloud.
122
- const journalHintSource = remoteJournalContext;
123
-
124
- const rl = readline.createInterface({
125
- input: process.stdin,
126
- output: process.stdout,
127
- });
128
-
129
- const ask = async (promptText, options = {}) => {
130
- const { allowEmpty = false } = options;
131
- while (true) {
132
- const answer = await new Promise((resolve) => rl.question(promptText, resolve));
133
- const trimmed = answer.trim();
134
- if (trimmed.toLowerCase() === 'exit') {
135
- throw brainstormAbortError();
136
- }
137
- if (!allowEmpty && trimmed === '') {
138
- console.log('Please enter a value (or type "exit" to abort).');
139
- continue;
140
- }
141
- return trimmed;
142
- }
143
- };
144
-
145
- const askYesNo = async (promptText) => {
146
- while (true) {
147
- const response = (await ask(promptText)).toLowerCase();
148
- if (response === 'y' || response === 'yes') return true;
149
- if (response === 'n' || response === 'no') return false;
150
- console.log('Please answer with "y" or "n" (or type "exit" to abort).');
151
- }
152
- };
153
-
154
- const collectList = async (label, options = {}) => {
155
- const { minimum = 0 } = options;
156
- const items = [];
157
- while (true) {
158
- const promptSuffix = items.length === 0 ? '' : ' (blank to finish)';
159
- const value = await ask(`${label} ${items.length + 1}${promptSuffix}: `, {
160
- allowEmpty: items.length >= minimum,
161
- });
162
- if (!value) {
163
- if (items.length < minimum) {
164
- console.log(`Please provide at least ${minimum} ${minimum === 1 ? 'item' : 'items'}.`);
165
- continue;
166
- }
167
- break;
168
- }
169
- items.push(value);
170
- }
171
- return items;
172
- };
173
-
174
- let selectedInboxItem = null;
175
- let topicSummary = '';
176
-
177
- try {
178
- let inboxItems = parseInboxItems(localJournalContext || '');
179
-
180
- if (topicFromArgs) {
181
- topicSummary = topicFromArgs;
182
- const newId = addInboxIdea(logFile, topicSummary);
183
- console.log(`✓ Added I${newId} to today\'s Inbox.`);
184
- selectedInboxItem = { id: newId, text: topicSummary };
185
- inboxItems = parseInboxItems(fs.readFileSync(logFile, 'utf8'));
186
- }
187
-
188
- if (topicFromArgs) {
189
- // Topic provided via CLI args — treat as a new brainstorm and skip source selection.
190
- } else if (inboxItems.length > 0) {
191
- console.log('Choose a brainstorm source:');
192
- console.log(' 1. Select an item from today\'s Inbox');
193
- console.log(' 2. Enter a new idea');
194
- console.log('');
195
-
196
- let choice;
197
- while (true) {
198
- choice = await ask('Choice (1-2): ');
199
- if (choice === '1' || choice === '2') {
200
- break;
201
- }
202
- console.log('Please enter 1 or 2.');
203
- }
204
-
205
- if (choice === '1') {
206
- console.log('');
207
- console.log('Today\'s Inbox:');
208
- inboxItems.forEach((item, index) => {
209
- console.log(` ${index + 1}. I${item.id} — ${item.text}`);
210
- });
211
- console.log('');
212
-
213
- while (true) {
214
- const selection = await ask(`Pick an item (1-${inboxItems.length}): `);
215
- const index = parseInt(selection, 10);
216
- if (!Number.isNaN(index) && index >= 1 && index <= inboxItems.length) {
217
- selectedInboxItem = inboxItems[index - 1];
218
- break;
219
- }
220
- console.log(`Enter a number between 1 and ${inboxItems.length}.`);
221
- }
222
-
223
- const editedSummary = await ask('Brainstorm topic (press Enter to keep original): ', { allowEmpty: true });
224
- topicSummary = editedSummary ? editedSummary : selectedInboxItem.text;
225
- } else {
226
- console.log('');
227
- topicSummary = await ask('Describe the brainstorm topic: ');
228
- const newId = addInboxIdea(logFile, topicSummary);
229
- console.log(`✓ Added I${newId} to today\'s Inbox.`);
230
- selectedInboxItem = { id: newId, text: topicSummary };
231
- }
232
- } else {
233
- console.log('No items in today\'s Inbox. Capture a new idea to begin.');
234
- topicSummary = await ask('Describe the brainstorm topic: ');
235
- const newId = addInboxIdea(logFile, topicSummary);
236
- console.log(`✓ Added I${newId} to today\'s Inbox.`);
237
- selectedInboxItem = { id: newId, text: topicSummary };
238
- }
239
-
240
- const sourceLabel = selectedInboxItem ? `I${selectedInboxItem.id}` : 'Ad-hoc';
241
-
242
- console.log('');
243
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
244
- console.log('📖 Step 1: Craft the Story');
245
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
246
- console.log('What should the output be? How should it feel?');
247
- console.log('This helps us capture the vision before diving into details.');
248
- console.log('');
249
-
250
- const userStory = await ask('Describe the desired outcome (what should users experience?): ');
251
- const feelingsVibe = await ask('Feelings/vibes we\'re aiming for? (optional): ', { allowEmpty: true });
252
-
253
- console.log('');
254
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
255
- console.log('🧠 Step 2: Brainstorm Session');
256
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
257
- console.log('Now let\'s uncover what we need to build.');
258
- console.log('');
259
-
260
- const constraints = await ask('Constraints or guardrails? (optional): ', { allowEmpty: true });
261
-
262
- // Build concise, spaced-out prompt (4-5 sentences max, lots of spacing)
263
- const promptLines = [];
264
-
265
- // Extract key snippets from journal if available (very brief)
266
- let journalHint = '';
267
- if (journalHintSource && journalHintSource.trim()) {
268
- const maxHint = 200;
269
- const lines = journalHintSource.split('\n').slice(0, 5).join(' ').trim();
270
- if (lines.length > maxHint) {
271
- journalHint = lines.substring(0, maxHint) + '...';
272
- } else {
273
- journalHint = lines;
274
- }
275
- }
276
-
277
- promptLines.push('You:');
278
- promptLines.push('');
279
- promptLines.push(`I want to brainstorm: ${topicSummary}`);
280
- promptLines.push('');
281
-
282
- if (userStory) {
283
- promptLines.push(`The outcome should be: ${userStory}`);
284
- promptLines.push('');
285
- }
286
-
287
- if (feelingsVibe) {
288
- promptLines.push(`Vibe we\'re going for: ${feelingsVibe}`);
289
- promptLines.push('');
290
- }
291
-
292
- if (journalHint) {
293
- promptLines.push(`Recent context: ${journalHint}`);
294
- promptLines.push('');
295
- }
296
-
297
- if (constraints) {
298
- promptLines.push(`Constraints: ${constraints}`);
299
- promptLines.push('');
300
- }
301
-
302
- promptLines.push('Help me uncover what we need to build. Keep responses short (4-5 sentences), pause for alignment, sketch ASCII when structure helps.');
303
- promptLines.push('');
304
- promptLines.push('Claude:');
305
-
306
- const promptText = promptLines.join('\n');
307
-
308
- console.log('');
309
- console.log('Copy this prompt into Claude Code (or your agent of choice):');
310
- console.log('');
311
- console.log('```');
312
- console.log(promptText);
313
- console.log('```');
314
- console.log('');
315
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
316
- console.log('💬 Brainstorm Mode — Thinking Together');
317
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
318
- console.log('');
319
- console.log('For the agent: Be conversational and supportive:');
320
- console.log(' • 3-4 sentences max per response');
321
- console.log(' • Ask ONE question at a time (never multiple)');
322
- console.log(' • Supportive tone: "That makes sense. What about X?"');
323
- console.log(' • No files created (exploration only)');
324
- console.log(' • User says "ready" or "plan" to exit brainstorm');
325
- console.log('');
326
- console.log('Example:');
327
- console.log(' User: "notifications but not sure"');
328
- console.log(' You: "What bothers you about current notifications?"');
329
- console.log(' User: "Easy to miss"');
330
- console.log(' You: "Makes sense. What if they stayed visible until dismissed?"');
331
- console.log('');
332
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
333
- console.log('');
334
-
335
- const logChoice = await askYesNo('Log this brainstorm session to today\'s journal? (y/n): ');
336
- if (logChoice) {
337
- const sessionSummary = await ask('Session summary (1-2 sentences): ');
338
- const nextStepsRaw = await ask('Next steps (optional, separate with ";"): ', { allowEmpty: true });
339
- const nextSteps = nextStepsRaw
340
- ? nextStepsRaw.split(';').map((item) => item.trim()).filter(Boolean)
341
- : [];
342
- try {
343
- recordBrainstormSession(
344
- logFile,
345
- sourceLabel,
346
- topicSummary,
347
- userStory,
348
- [],
349
- [],
350
- constraints,
351
- '',
352
- feelingsVibe || '',
353
- nextSteps,
354
- sessionSummary
355
- );
356
- if (selectedInboxItem) {
357
- const archive = await askYesNo('Archive this Inbox idea now? (y/n): ');
358
- if (archive) {
359
- try {
360
- let latestContent = fs.readFileSync(logFile, 'utf8');
361
- latestContent = removeInboxItemFromContent(latestContent, selectedInboxItem.id);
362
- if (typeof latestContent !== 'string') {
363
- throw new Error('Archive operation produced invalid journal content.');
364
- }
365
- writeJournalFile(logFile, latestContent);
366
- console.log(`✓ Archived I${selectedInboxItem.id} from Inbox.`);
367
- } catch (error) {
368
- console.log(`Could not archive I${selectedInboxItem.id}: ${error.message}`);
369
- }
370
- }
371
- }
372
- console.log('✓ Brainstorm session logged.');
373
- } catch (error) {
374
- if (error && error.__brainstormAbort) {
375
- throw error;
376
- }
377
- console.log(`Could not log brainstorm session: ${error.message}`);
378
- }
379
- } else {
380
- console.log('Skipped journaling. Prompt is ready for your agent.');
381
- }
382
-
383
- console.log('\nBrainstorm complete.');
384
- } finally {
385
- rl.close();
386
- }
387
25
  }
388
26
 
389
- function brainstormAbortError() {
390
- const error = new Error('Brainstorm cancelled by user.');
391
- error.__brainstormAbort = true;
392
- return error;
27
+ function topicFromArgs(args) {
28
+ return args.filter((arg) => !String(arg).startsWith('-')).join(' ').trim() || null;
393
29
  }
394
30
 
395
- function removeInboxItemFromContent(content, id) {
396
- const items = parseInboxItems(content).filter((item) => item.id !== id);
397
- return replaceInboxSection(content, items);
31
+ function journalRel(logFile) {
32
+ return path.relative(process.cwd(), logFile) || logFile;
398
33
  }
399
34
 
400
- function writeJournalFile(logFile, content) {
401
- if (typeof logFile !== 'string' || logFile.trim() === '') {
402
- throw new Error('Journal log file path is required.');
403
- }
404
- if (typeof content !== 'string') {
405
- throw new Error('Journal content must be a string.');
406
- }
407
- const logDir = path.dirname(logFile);
408
- const tempFile = path.join(logDir, `.${path.basename(logFile)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
409
- let tempFileWritten = false;
410
- try {
411
- fs.mkdirSync(logDir, { recursive: true });
412
- fs.writeFileSync(tempFile, content, { encoding: 'utf8', flag: 'wx' });
413
- tempFileWritten = true;
414
- fs.renameSync(tempFile, logFile);
415
- } catch (error) {
416
- if (tempFileWritten || !error || error.code !== 'EEXIST') {
417
- try {
418
- fs.unlinkSync(tempFile);
419
- } catch {}
420
- }
421
- const message = error && error.message ? error.message : String(error);
422
- throw new Error(`Could not write journal file: ${message}`);
35
+ function printCaptured(id, text, rel, args) {
36
+ const inboxId = `I${id}`;
37
+ if (wantsJson(args)) {
38
+ const payload = compactSuccessPayload({
39
+ action: 'captured',
40
+ ids: {
41
+ id,
42
+ inbox_id: inboxId,
43
+ text,
44
+ journal: rel,
45
+ },
46
+ next_command: 'atris plan',
47
+ });
48
+ printCliJson(payload, payload, args);
49
+ return;
423
50
  }
51
+ console.log(`captured ${inboxId}: ${text}`);
52
+ console.log(`journal: ${rel}`);
53
+ console.log('Next: atris plan');
424
54
  }
425
55
 
426
- function insertIntoNotesSection(content, block) {
427
- const regex = /(## Notes\n)([\s\S]*?)(\n---|\n##|$)/;
428
- const match = content.match(regex);
429
- if (!match) {
430
- return `${content}\n\n## Notes\n\n${block}\n`;
56
+ function printIdeaRequired(rel, args) {
57
+ if (wantsJson(args)) {
58
+ const payload = compactErrorPayload({
59
+ reason: 'idea_required',
60
+ detail: 'brainstorm needs an idea on the command line',
61
+ next_command: 'atris brainstorm "<idea>"',
62
+ });
63
+ printCliJson(payload, payload, args);
64
+ return;
431
65
  }
432
- const header = match[1];
433
- const body = match[2];
434
- const suffix = match[3];
435
- const trimmedBody = body.replace(/\s*$/, '');
436
- const newBody = trimmedBody
437
- ? `${trimmedBody}\n\n${block}\n`
438
- : `\n${block}\n`;
439
- return content.replace(regex, `${header}${newBody}${suffix}`);
440
- }
441
-
442
- function getTimeLabel() {
443
- const now = new Date();
444
- const hours = String(now.getHours()).padStart(2, '0');
445
- const minutes = String(now.getMinutes()).padStart(2, '0');
446
- return `${hours}:${minutes}`;
66
+ console.log('brainstorm captures an idea on the command line and exits.');
67
+ console.log(`journal: ${rel}`);
68
+ console.log('Next: atris brainstorm "<idea>"');
447
69
  }
448
70
 
449
- function recordBrainstormSession(
450
- logFile,
451
- sourceLabel,
452
- topic,
453
- desiredOutcome,
454
- keyQuestions,
455
- focusAreas,
456
- constraints,
457
- references,
458
- tonePreference,
459
- nextSteps,
460
- sessionSummary
461
- ) {
462
- let content = fs.readFileSync(logFile, 'utf8');
463
- const lines = [
464
- `### Brainstorm Session — ${getTimeLabel()}`,
465
- `**Source:** ${sourceLabel}`,
466
- `**Topic:** ${topic}`,
467
- ];
468
- if (desiredOutcome) {
469
- lines.push(`**User Story / Desired Outcome:** ${desiredOutcome}`);
470
- }
471
- if (tonePreference) {
472
- lines.push(`**Vibe / Feelings:** ${tonePreference}`);
473
- }
474
- if (keyQuestions && keyQuestions.length > 0) {
475
- lines.push('**Key Questions:**');
476
- keyQuestions.forEach((item) => lines.push(`- ${item}`));
477
- }
478
- if (focusAreas && focusAreas.length > 0) {
479
- lines.push('**Focus Areas:**');
480
- focusAreas.forEach((item) => lines.push(`- ${item}`));
481
- }
482
- if (constraints) {
483
- lines.push(`**Constraints:** ${constraints}`);
71
+ async function brainstormAtris() {
72
+ const args = process.argv.slice(3);
73
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
74
+ printHelp();
75
+ return;
484
76
  }
485
- if (references) {
486
- lines.push(`**Context / References:** ${references}`);
77
+
78
+ const root = process.cwd();
79
+ if (isFreshWorkspace(root)) {
80
+ process.exit(speakFirstMinute({ root, fresh: true, asJson: wantsJson(args) }));
487
81
  }
488
- if (sessionSummary) {
489
- lines.push(`**Session Summary:** ${sessionSummary}`);
82
+
83
+ ensureLogDirectory();
84
+ const { logFile, dateFormatted } = getLogPath();
85
+ if (!fs.existsSync(logFile)) {
86
+ createLogFile(logFile, dateFormatted);
490
87
  }
491
- if (nextSteps && nextSteps.length > 0) {
492
- lines.push('**Next Steps:**');
493
- nextSteps.forEach((item) => lines.push(`- ${item}`));
88
+
89
+ const idea = topicFromArgs(args);
90
+ const rel = journalRel(logFile);
91
+
92
+ // Named explore path: capture and exit. A TTY must not open a wizard.
93
+ // Scar 2026-08-24: wrote I1, then hung on "Describe the desired outcome".
94
+ if (idea) {
95
+ const newId = addInboxIdea(logFile, idea);
96
+ printCaptured(newId, idea, rel, args);
97
+ return;
494
98
  }
495
99
 
496
- const block = lines.join('\n');
497
- content = insertIntoNotesSection(content, block);
498
- writeJournalFile(logFile, content);
100
+ printIdeaRequired(rel, args);
499
101
  }
500
102
 
501
-
502
103
  module.exports = {
503
- brainstormAtris
104
+ brainstormAtris,
504
105
  };