atris 3.38.0 → 3.41.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 (78) hide show
  1. package/AGENTS.md +25 -6
  2. package/atris/PERSONA.md +8 -4
  3. package/atris.md +7 -0
  4. package/ax +2 -1
  5. package/bin/atris.js +31 -6
  6. package/commands/agent-spawn.js +13 -11
  7. package/commands/autoland.js +28 -77
  8. package/commands/bench.js +10 -12
  9. package/commands/business.js +345 -0
  10. package/commands/chat-scan.js +5 -7
  11. package/commands/codex-goal.js +8 -10
  12. package/commands/computer.js +20 -0
  13. package/commands/console.js +19 -3
  14. package/commands/decide.js +166 -0
  15. package/commands/deck.js +1 -4
  16. package/commands/drill.js +14 -24
  17. package/commands/engine.js +196 -8
  18. package/commands/gm.js +8 -6
  19. package/commands/harvest.js +1 -4
  20. package/commands/init.js +23 -3
  21. package/commands/land.js +8 -14
  22. package/commands/launchpad.js +1 -14
  23. package/commands/lifecycle.js +5 -5
  24. package/commands/log.js +55 -5
  25. package/commands/member.js +558 -574
  26. package/commands/mission.js +456 -237
  27. package/commands/pack.js +2746 -164
  28. package/commands/play.js +6 -4
  29. package/commands/probe.js +2 -2
  30. package/commands/pulse.js +15 -16
  31. package/commands/release.js +10 -9
  32. package/commands/router.js +5 -4
  33. package/commands/site-deploy.js +885 -0
  34. package/commands/site.js +11 -2
  35. package/commands/slop.js +14 -2
  36. package/commands/stream.js +4 -18
  37. package/commands/task.js +880 -558
  38. package/commands/taste.js +101 -0
  39. package/commands/team.js +176 -3
  40. package/commands/vercel.js +4 -2
  41. package/commands/voice.js +195 -0
  42. package/commands/watch.js +1 -22
  43. package/commands/wiki.js +1 -4
  44. package/commands/workflow.js +2 -2
  45. package/commands/worktree.js +1 -14
  46. package/commands/xp.js +27 -24
  47. package/lib/accept-verify-gate.js +5 -1
  48. package/lib/arg-parser.js +41 -0
  49. package/lib/auto-accept-certified.js +116 -1
  50. package/lib/autoland.js +66 -0
  51. package/lib/bench/runner.js +19 -1
  52. package/lib/context-gatherer.js +7 -1
  53. package/lib/engine-registry.js +141 -20
  54. package/lib/falsifier-probe.js +84 -0
  55. package/lib/fleet.js +65 -15
  56. package/lib/git-spawn.js +15 -0
  57. package/lib/json-file.js +37 -0
  58. package/lib/known-commands.js +2 -2
  59. package/lib/lesson-preflight.js +146 -0
  60. package/lib/loop-doctor.js +0 -2
  61. package/lib/mission-human-asks.js +28 -0
  62. package/lib/mission-protected-lane.js +4 -1
  63. package/lib/official-cli-integration.js +47 -2
  64. package/lib/orb-context.js +8 -1
  65. package/lib/pack-capabilities.js +685 -0
  66. package/lib/router-brain.js +51 -1
  67. package/lib/runner-command.js +0 -6
  68. package/lib/self-drive.js +44 -13
  69. package/lib/task-db.js +137 -3
  70. package/lib/task-decision.js +50 -0
  71. package/lib/taste-lessons.js +153 -0
  72. package/lib/tool-result-encode.js +17 -1
  73. package/lib/voice-gate.js +66 -0
  74. package/lib/wish-audit.js +1 -1
  75. package/lib/wish-delegate.js +1 -1
  76. package/lib/zip.js +95 -7
  77. package/package.json +2 -1
  78. package/templates/business-starter/persona.md +9 -0
@@ -2028,6 +2028,328 @@ function activityBar(daysSinceActive, width = 10) {
2028
2028
  return '\u2501'.repeat(filled) + '\u2591'.repeat(width - filled);
2029
2029
  }
2030
2030
 
2031
+ function asRecord(value) {
2032
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
2033
+ }
2034
+
2035
+ function firstString(...values) {
2036
+ for (const value of values) {
2037
+ if (typeof value === 'string' && value.trim()) return value.trim();
2038
+ }
2039
+ return '';
2040
+ }
2041
+
2042
+ function firstNumber(...values) {
2043
+ for (const value of values) {
2044
+ const parsed = typeof value === 'number'
2045
+ ? value
2046
+ : (typeof value === 'string' && value.trim() ? Number(value) : Number.NaN);
2047
+ if (Number.isFinite(parsed)) return parsed;
2048
+ }
2049
+ return 0;
2050
+ }
2051
+
2052
+ function firstBoolean(...values) {
2053
+ for (const value of values) {
2054
+ if (typeof value === 'boolean') return value;
2055
+ if (value === 1 || value === '1' || value === 'true') return true;
2056
+ if (value === 0 || value === '0' || value === 'false') return false;
2057
+ }
2058
+ return false;
2059
+ }
2060
+
2061
+ function normalizeOrdersPayload(value) {
2062
+ const root = asRecord(value);
2063
+ const body = asRecord(root.data);
2064
+ const source = Object.keys(body).length > 0 ? body : root;
2065
+ const summary = asRecord(source.summary);
2066
+ const rawOrders = Array.isArray(value)
2067
+ ? value
2068
+ : (Array.isArray(source.orders) ? source.orders : (Array.isArray(root.orders) ? root.orders : []));
2069
+ const orders = rawOrders.map((rawOrder, index) => {
2070
+ const order = asRecord(rawOrder);
2071
+ const product = asRecord(order.product);
2072
+ const item = asRecord(order.item);
2073
+ const buyer = asRecord(order.buyer);
2074
+ const customer = asRecord(order.customer);
2075
+ const metadata = asRecord(order.metadata);
2076
+ const firstItem = Array.isArray(order.items) ? asRecord(order.items[0]) : {};
2077
+ const status = firstString(order.status, order.payment_status, order.order_type).toLowerCase();
2078
+ const createdAt = firstString(
2079
+ order.created_at,
2080
+ order.createdAt,
2081
+ order.order_date,
2082
+ order.date,
2083
+ order.paid_at,
2084
+ ) || null;
2085
+
2086
+ return {
2087
+ id: firstString(order.id, order.order_id, order.reference) || `order-${index + 1}`,
2088
+ reference: firstString(order.order_ref, order.reference, order.order_number, order.id) || `order ${index + 1}`,
2089
+ product: firstString(
2090
+ order.product_name,
2091
+ order.product_title,
2092
+ typeof order.product === 'string' ? order.product : null,
2093
+ product.name,
2094
+ product.title,
2095
+ item.name,
2096
+ firstItem.name,
2097
+ firstItem.product_name,
2098
+ metadata.product_name,
2099
+ ) || 'product',
2100
+ quantity: Math.max(1, Math.round(firstNumber(order.quantity, order.qty, firstItem.quantity, 1))),
2101
+ buyer: firstString(
2102
+ order.buyer_name,
2103
+ order.customer_name,
2104
+ buyer.name,
2105
+ customer.name,
2106
+ order.buyer_email,
2107
+ order.customer_email,
2108
+ buyer.email,
2109
+ customer.email,
2110
+ ) || 'guest',
2111
+ status: status || 'unknown',
2112
+ isPreorder: firstBoolean(order.is_preorder, order.preorder)
2113
+ || status.replace(/[_-]/g, '').includes('preorder')
2114
+ || status === 'pending',
2115
+ amountCents: firstNumber(order.amount_cents, order.price_cents, order.total_cents, firstItem.amount_cents),
2116
+ createdAt,
2117
+ };
2118
+ });
2119
+
2120
+ orders.sort((a, b) => {
2121
+ const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
2122
+ const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
2123
+ return (Number.isFinite(bTime) ? bTime : 0) - (Number.isFinite(aTime) ? aTime : 0);
2124
+ });
2125
+
2126
+ return {
2127
+ orders,
2128
+ revenueCents: firstNumber(summary.revenue_cents, source.revenue_cents, root.revenue_cents),
2129
+ paidOrders: Math.max(0, Math.round(firstNumber(summary.paid_orders, source.paid_orders, root.paid_orders))),
2130
+ pendingRevenueCents: firstNumber(
2131
+ summary.pending_revenue_cents,
2132
+ source.pending_revenue_cents,
2133
+ root.pending_revenue_cents,
2134
+ ),
2135
+ };
2136
+ }
2137
+
2138
+ function readStorefront(business) {
2139
+ const config = asRecord(asRecord(business).config);
2140
+ const storefront = asRecord(config.storefront);
2141
+ return {
2142
+ enabled: storefront.enabled === true,
2143
+ products: Array.isArray(storefront.products) ? storefront.products.filter(product => product && typeof product === 'object') : [],
2144
+ };
2145
+ }
2146
+
2147
+ function readWalletNet(business) {
2148
+ const config = asRecord(asRecord(business).config);
2149
+ for (const key of ['wallet_balance', 'credits_balance', 'credits', 'wallet_credits']) {
2150
+ const value = config[key];
2151
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
2152
+ }
2153
+ return null;
2154
+ }
2155
+
2156
+ function formatWalletNet(value) {
2157
+ if (value === null) return 'not available';
2158
+ return `$${value.toLocaleString('en-US', { maximumFractionDigits: 2 })}`;
2159
+ }
2160
+
2161
+ function formatCents(value, currency = 'usd') {
2162
+ const amount = firstNumber(value) / 100;
2163
+ const code = firstString(currency).toUpperCase() || 'USD';
2164
+ const formatted = amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
2165
+ return code === 'USD' ? `$${formatted}` : `${code} ${formatted}`;
2166
+ }
2167
+
2168
+ function businessOperationError(message) {
2169
+ console.error(message);
2170
+ process.exitCode = 1;
2171
+ return null;
2172
+ }
2173
+
2174
+ async function resolveBusinessOperation(slug, usage) {
2175
+ const requestedSlug = detectBusinessSlug(slug);
2176
+ if (!requestedSlug) return businessOperationError(`No business specified. Usage: ${usage}`);
2177
+
2178
+ const creds = loadCredentials();
2179
+ if (!creds || !creds.token) return businessOperationError('Not logged in. Run: atris login');
2180
+
2181
+ const resolved = await resolveSlug(requestedSlug, creds);
2182
+ if (!resolved) return businessOperationError(`Business "${requestedSlug}" not found.`);
2183
+ return { requestedSlug, resolved, token: creds.token };
2184
+ }
2185
+
2186
+ async function fetchBusinessDashboard(context) {
2187
+ const result = await apiRequestJson(`/business/${context.resolved.business_id}/dashboard`, {
2188
+ method: 'GET',
2189
+ token: context.token,
2190
+ timeoutMs: 120000,
2191
+ });
2192
+ if (!result.ok) {
2193
+ const detail = result.errorMessage || result.error || `HTTP ${result.status}`;
2194
+ return businessOperationError(`Could not load business room: ${detail}`);
2195
+ }
2196
+ return result.data || {};
2197
+ }
2198
+
2199
+ async function fetchBusinessOrders(context, limit = 50) {
2200
+ return apiRequestJson(`/storefront/${context.resolved.business_id}/orders?limit=${limit}`, {
2201
+ method: 'GET',
2202
+ token: context.token,
2203
+ timeoutMs: 120000,
2204
+ });
2205
+ }
2206
+
2207
+ function renderOrderLine(order) {
2208
+ const kind = order.isPreorder ? ', preorder' : '';
2209
+ const when = order.createdAt ? relativeTime(order.createdAt) : 'date unavailable';
2210
+ return ` ${order.reference}: ${order.product} x${order.quantity}, ${order.buyer}, ${when}${kind}`;
2211
+ }
2212
+
2213
+ async function businessRoom(slug) {
2214
+ const context = await resolveBusinessOperation(slug, 'atris business room <slug>');
2215
+ if (!context) return;
2216
+
2217
+ const [dashboard, ordersResult] = await Promise.all([
2218
+ fetchBusinessDashboard(context),
2219
+ fetchBusinessOrders(context, 5),
2220
+ ]);
2221
+ if (!dashboard) return;
2222
+
2223
+ const business = asRecord(dashboard.business);
2224
+ const roster = asRecord(dashboard.roster);
2225
+ const members = Array.isArray(roster.members)
2226
+ ? roster.members
2227
+ : (Array.isArray(business.members) ? business.members : []);
2228
+ const storefront = readStorefront(business);
2229
+ const orders = ordersResult.ok ? normalizeOrdersPayload(ordersResult.data).orders.slice(0, 5) : [];
2230
+ const name = business.name || context.resolved.name || context.requestedSlug;
2231
+ const appCount = Number.isFinite(business.app_count)
2232
+ ? business.app_count
2233
+ : (Array.isArray(business.apps) ? business.apps.length : 0);
2234
+
2235
+ console.log('');
2236
+ console.log(`business room: ${name}`);
2237
+ console.log(`wallet/net: ${formatWalletNet(readWalletNet(business))}`);
2238
+ console.log(`members: ${members.length}`);
2239
+ for (const member of members) {
2240
+ const agent = asRecord(member.atris);
2241
+ const memberName = firstString(member.display_name, agent.agent_name, member.name, member.email) || 'unknown';
2242
+ console.log(` ${memberName} (${member.role || 'member'})`);
2243
+ }
2244
+ console.log(`apps: ${appCount}`);
2245
+ console.log(`store: ${storefront.enabled ? 'enabled' : 'disabled'}, ${storefront.products.length} products`);
2246
+ console.log('recent orders:');
2247
+ if (!ordersResult.ok) {
2248
+ const detail = ordersResult.errorMessage || ordersResult.error || `HTTP ${ordersResult.status}`;
2249
+ console.log(` unavailable: ${detail}`);
2250
+ process.exitCode = 1;
2251
+ } else if (orders.length === 0) {
2252
+ console.log(' none');
2253
+ } else {
2254
+ orders.forEach(order => console.log(renderOrderLine(order)));
2255
+ }
2256
+ console.log('');
2257
+ }
2258
+
2259
+ async function businessProducts(slug) {
2260
+ const context = await resolveBusinessOperation(slug, 'atris business products <slug>');
2261
+ if (!context) return;
2262
+ const dashboard = await fetchBusinessDashboard(context);
2263
+ if (!dashboard) return;
2264
+
2265
+ const business = asRecord(dashboard.business);
2266
+ const storefront = readStorefront(business);
2267
+ const name = business.name || context.resolved.name || context.requestedSlug;
2268
+ console.log('');
2269
+ console.log(`products: ${name} (${storefront.products.length})`);
2270
+ if (storefront.products.length === 0) {
2271
+ console.log(' none');
2272
+ } else {
2273
+ for (const product of storefront.products) {
2274
+ const productName = firstString(product.name) || 'unnamed product';
2275
+ const productId = firstString(product.id) || 'no id';
2276
+ console.log(` ${productName} (${productId}): ${formatCents(product.price_cents, product.currency)}`);
2277
+ }
2278
+ }
2279
+ console.log('');
2280
+ }
2281
+
2282
+ async function businessOrders(slug) {
2283
+ const context = await resolveBusinessOperation(slug, 'atris business orders <slug>');
2284
+ if (!context) return;
2285
+ const result = await fetchBusinessOrders(context, 50);
2286
+ if (!result.ok) {
2287
+ const detail = result.errorMessage || result.error || `HTTP ${result.status}`;
2288
+ businessOperationError(`Could not load orders: ${detail}`);
2289
+ return;
2290
+ }
2291
+
2292
+ const data = normalizeOrdersPayload(result.data);
2293
+ const name = context.resolved.name || context.requestedSlug;
2294
+ console.log('');
2295
+ console.log(`orders: ${name} (${data.orders.length})`);
2296
+ console.log(`revenue: ${formatCents(data.revenueCents)}`);
2297
+ console.log(`paid orders: ${data.paidOrders}`);
2298
+ console.log(`pending revenue: ${formatCents(data.pendingRevenueCents)}`);
2299
+ if (data.orders.length === 0) {
2300
+ console.log(' none');
2301
+ } else {
2302
+ data.orders.forEach(order => console.log(renderOrderLine(order)));
2303
+ }
2304
+ console.log('');
2305
+ }
2306
+
2307
+ function parseBusinessStoreArgs(args = []) {
2308
+ const actions = new Set(['status', 'on', 'enable', 'toggle', 'off', 'disable']);
2309
+ if (actions.has(args[0])) return { action: args[0], slug: args[1] };
2310
+ if (actions.has(args[1])) return { action: args[1], slug: args[0] };
2311
+ return { action: 'status', slug: args[0] };
2312
+ }
2313
+
2314
+ async function businessStore(args = []) {
2315
+ const { action, slug } = parseBusinessStoreArgs(args);
2316
+ if (action === 'off' || action === 'disable') {
2317
+ businessOperationError('The production store API does not support disabling stores yet.');
2318
+ return;
2319
+ }
2320
+
2321
+ const context = await resolveBusinessOperation(slug, 'atris business store <on|status> [slug]');
2322
+ if (!context) return;
2323
+ const dashboard = await fetchBusinessDashboard(context);
2324
+ if (!dashboard) return;
2325
+
2326
+ const business = asRecord(dashboard.business);
2327
+ const storefront = readStorefront(business);
2328
+ const name = business.name || context.resolved.name || context.requestedSlug;
2329
+ if (action === 'status') {
2330
+ console.log(`store: ${storefront.enabled ? 'enabled' : 'disabled'} for ${name}, ${storefront.products.length} products`);
2331
+ return;
2332
+ }
2333
+ if (storefront.enabled) {
2334
+ console.log(`store: already enabled for ${name}, ${storefront.products.length} products`);
2335
+ return;
2336
+ }
2337
+
2338
+ const result = await apiRequestJson(`/storefront/${context.resolved.business_id}/products`, {
2339
+ method: 'PUT',
2340
+ token: context.token,
2341
+ body: { products: storefront.products },
2342
+ timeoutMs: 120000,
2343
+ });
2344
+ if (!result.ok) {
2345
+ const detail = result.errorMessage || result.error || `HTTP ${result.status}`;
2346
+ businessOperationError(`Could not enable store: ${detail}`);
2347
+ return;
2348
+ }
2349
+ const enabledProducts = Array.isArray(result.data?.products) ? result.data.products.length : storefront.products.length;
2350
+ console.log(`store: enabled for ${name}, ${enabledProducts} products`);
2351
+ }
2352
+
2031
2353
  // ---------------------------------------------------------------------------
2032
2354
  // atris business health <slug>
2033
2355
  // ---------------------------------------------------------------------------
@@ -3045,6 +3367,10 @@ function printBusinessHelp() {
3045
3367
  console.log(' simulate <idea> Create a business, four-role team, missions, and endgame loop');
3046
3368
  console.log(' add <slug> Register an existing cloud business');
3047
3369
  console.log(' list Show registered businesses');
3370
+ console.log(' room [slug] Show wallet, members, apps, store, and recent orders');
3371
+ console.log(' products [slug] List store products');
3372
+ console.log(' orders [slug] List store orders');
3373
+ console.log(' store on [slug] Enable the store with its current catalog');
3048
3374
  console.log(' team [slug] Show members, roles, and admin access');
3049
3375
  console.log(' status <slug> Quick status check');
3050
3376
  console.log(' health [slug] Full health dashboard');
@@ -3122,6 +3448,21 @@ async function businessCommand(subcommand, ...args) {
3122
3448
  case 'health':
3123
3449
  await businessHealth(args[0]);
3124
3450
  break;
3451
+ case 'room':
3452
+ await businessRoom(args[0]);
3453
+ break;
3454
+ case 'products':
3455
+ case 'product':
3456
+ await businessProducts(args[0]);
3457
+ break;
3458
+ case 'orders':
3459
+ case 'order':
3460
+ await businessOrders(args[0]);
3461
+ break;
3462
+ case 'store':
3463
+ case 'storefront':
3464
+ await businessStore(args);
3465
+ break;
3125
3466
  case 'team':
3126
3467
  case 'members':
3127
3468
  case 'roster':
@@ -3189,4 +3530,8 @@ module.exports = {
3189
3530
  collectBusinessShareState,
3190
3531
  renderBusinessCreatedNextSteps,
3191
3532
  recordBusinessRun,
3533
+ normalizeOrdersPayload,
3534
+ parseBusinessStoreArgs,
3535
+ readStorefront,
3536
+ readWalletNet,
3192
3537
  };
@@ -1,12 +1,10 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { hasFlag } = require('../lib/arg-parser');
3
4
  const { scanChatLogs, writeLatestScan } = require('../lib/chat-log-scan');
4
5
 
5
- function hasFlag(args, name) {
6
- return args.includes(name);
7
- }
8
-
9
- function readFlag(args, name, fallback) {
6
+ // Preserve the existing rule that the next token is a value, even if it is a flag.
7
+ function readFollowingFlag(args, name, fallback) {
10
8
  const i = args.indexOf(name);
11
9
  if (i === -1 || i === args.length - 1) return fallback;
12
10
  return args[i + 1];
@@ -79,8 +77,8 @@ Options:
79
77
 
80
78
  const report = scanChatLogs({
81
79
  cwd: root,
82
- hours: readFlag(args, '--hours', 720),
83
- limit: readFlag(args, '--limit', 12),
80
+ hours: readFollowingFlag(args, '--hours', 720),
81
+ limit: readFollowingFlag(args, '--limit', 12),
84
82
  });
85
83
 
86
84
  let latestPath = null;
@@ -2,15 +2,13 @@ const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
4
  const { spawnSync } = require('child_process');
5
+ const { hasFlag } = require('../lib/arg-parser');
5
6
 
6
7
  const SCHEMA = 'atris.codex_goal.v1';
7
8
  const CONFIRM_RESET_FLAG = '--confirm-complete-goal-reset';
8
9
 
9
- function hasFlag(args, name) {
10
- return args.includes(name);
11
- }
12
-
13
- function readFlag(args, name, fallback = '') {
10
+ // Preserve the existing rule that the next token is a value, even if it is a flag.
11
+ function readFollowingFlag(args, name, fallback = '') {
14
12
  const index = args.indexOf(name);
15
13
  if (index === -1 || index + 1 >= args.length) return fallback;
16
14
  return args[index + 1];
@@ -32,7 +30,7 @@ function sqlString(value) {
32
30
  }
33
31
 
34
32
  function resolveStatePath(args = []) {
35
- const explicit = readFlag(args, '--state', process.env.CODEX_STATE_DB || '');
33
+ const explicit = readFollowingFlag(args, '--state', process.env.CODEX_STATE_DB || '');
36
34
  if (explicit) return path.resolve(expandHome(explicit));
37
35
  // Codex moved native goals into ~/.codex/goals_1.sqlite; older builds kept them in state_5.sqlite.
38
36
  // Prefer the live goals DB so the bridge sees real goal activity, fall back to the legacy state DB.
@@ -43,7 +41,7 @@ function resolveStatePath(args = []) {
43
41
 
44
42
  // Thread metadata (cwd/title) stayed in state_5.sqlite even after goals moved to goals_1.sqlite.
45
43
  function resolveThreadsPath(args = []) {
46
- const explicit = readFlag(args, '--threads-db', process.env.CODEX_THREADS_DB || '');
44
+ const explicit = readFollowingFlag(args, '--threads-db', process.env.CODEX_THREADS_DB || '');
47
45
  if (explicit) return path.resolve(expandHome(explicit));
48
46
  const legacyDb = path.join(os.homedir(), '.codex', 'state_5.sqlite');
49
47
  return fs.existsSync(legacyDb) ? path.resolve(legacyDb) : '';
@@ -79,7 +77,7 @@ function runGoalQuery(args, buildSql) {
79
77
  }
80
78
 
81
79
  function defaultRunsDir(args = []) {
82
- return path.resolve(readFlag(args, '--out-dir', path.join(process.cwd(), '.atris', 'runs')));
80
+ return path.resolve(readFollowingFlag(args, '--out-dir', path.join(process.cwd(), '.atris', 'runs')));
83
81
  }
84
82
 
85
83
  function ensurePrivateDir(dir) {
@@ -196,7 +194,7 @@ function readRecentGoals(args, limit = 10) {
196
194
  }
197
195
 
198
196
  function resolveThreadGoal(args) {
199
- const explicitThread = readFlag(args, '--thread', '');
197
+ const explicitThread = readFollowingFlag(args, '--thread', '');
200
198
  if (explicitThread) return readGoalByThread(args, explicitThread);
201
199
  if (hasFlag(args, '--latest')) return readLatestGoalForCwd(args, process.cwd());
202
200
  const envThread = process.env.CODEX_THREAD_ID || '';
@@ -248,7 +246,7 @@ function statusCommand(args) {
248
246
  return;
249
247
  }
250
248
 
251
- const limit = Math.max(1, Math.min(50, Number(readFlag(args, '--limit', '10')) || 10));
249
+ const limit = Math.max(1, Math.min(50, Number(readFollowingFlag(args, '--limit', '10')) || 10));
252
250
  const goals = readRecentGoals(args, limit);
253
251
  const payload = { ok: true, schema: SCHEMA, action: 'status', state_path: dbPath, goals };
254
252
  printJsonOrText(payload, [
@@ -4384,6 +4384,7 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
4384
4384
  default:
4385
4385
  console.error(`Unknown subcommand: ${sub}`);
4386
4386
  console.log('Run: atris computer --help');
4387
+ process.exitCode = 1;
4387
4388
  }
4388
4389
  }
4389
4390
 
@@ -4399,4 +4400,23 @@ module.exports = {
4399
4400
  extractAttachedWorkspaceMismatch,
4400
4401
  contextForAttachedWorkspaceMismatch,
4401
4402
  printRecruitingLocalSyncOutcome,
4403
+ // Hermetic parsing/formatting layer, exported for test/computer.test.js.
4404
+ parseComputerOptions,
4405
+ parseComputerCreateArgs,
4406
+ computerCreateArgsHaveName,
4407
+ normalizeComputerType,
4408
+ formatComputerTypeList,
4409
+ parseComputerDeleteArgs,
4410
+ parseComputerCardArgs,
4411
+ renderComputerCard,
4412
+ renderComputerCardMarkdown,
4413
+ formatLeaseAge,
4414
+ formatWorkspaceRef,
4415
+ workspaceMatchesInput,
4416
+ resolveWorkspaceFromList,
4417
+ workspaceMatchesComputerType,
4418
+ looksLikeWorkspaceId,
4419
+ shellQuote,
4420
+ withoutRecruitingWrapperFlags,
4421
+ formatCloudSelection,
4402
4422
  };
@@ -315,18 +315,34 @@ function checkAuth(backend) {
315
315
  function launchClaude(systemPrompt, extraArgs, options = {}) {
316
316
  const skipPermissions = options.skipPermissions !== false;
317
317
  const runnerBin = resolveClaudeRunnerBin();
318
+ const promptStdin = typeof options.promptStdin === 'string'
319
+ ? options.promptStdin
320
+ : null;
318
321
  const args = [
319
322
  ...(skipPermissions ? ['--dangerously-skip-permissions'] : []),
323
+ ...(promptStdin !== null ? ['--print'] : []),
320
324
  '--append-system-prompt', systemPrompt,
321
325
  ...extraArgs,
322
326
  ];
323
327
 
324
328
  const child = spawnSync(runnerBin, args, {
325
329
  cwd: process.cwd(),
326
- stdio: 'inherit',
327
- env: { ...process.env, CLAUDECODE: undefined },
330
+ stdio: promptStdin === null ? 'inherit' : ['pipe', 'inherit', 'inherit'],
331
+ ...(promptStdin === null ? {} : { input: promptStdin }),
332
+ env: { ...process.env, ...(options.runnerEnv || {}), CLAUDECODE: undefined },
328
333
  });
329
334
 
335
+ try {
336
+ if (typeof options.onRunnerExit === 'function') {
337
+ options.onRunnerExit({ status: child.status ?? 1, signal: child.signal || null });
338
+ }
339
+ } catch (error) {
340
+ console.error(`warning: could not finalize runner receipt: ${error.message}`);
341
+ }
342
+ for (const cleanupPath of options.cleanupPaths || []) {
343
+ try { fs.rmSync(cleanupPath, { recursive: true, force: true }); } catch {}
344
+ }
345
+
330
346
  if (child.error) {
331
347
  console.error(`✗ Failed to start ${runnerBin}: ${child.error.message}`);
332
348
  process.exit(1);
@@ -422,7 +438,7 @@ function consoleCommand(options = {}) {
422
438
 
423
439
  // Launch
424
440
  if (backend === 'claude') {
425
- launchClaude(systemPrompt, extraArgs, { skipPermissions });
441
+ launchClaude(systemPrompt, extraArgs, { ...options, skipPermissions });
426
442
  } else {
427
443
  launchCodex(systemPrompt, extraArgs);
428
444
  }