atris 3.37.1 → 3.40.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 (164) hide show
  1. package/AGENTS.md +25 -6
  2. package/README.md +2 -0
  3. package/atris/PERSONA.md +11 -11
  4. package/atris.md +17 -2
  5. package/ax +160 -34
  6. package/bin/atris.js +78 -6
  7. package/commands/activate.js +16 -3
  8. package/commands/agent-spawn.js +13 -18
  9. package/commands/autoland.js +76 -47
  10. package/commands/autopilot-front.js +0 -4
  11. package/commands/autopilot.js +0 -12
  12. package/commands/bench.js +10 -12
  13. package/commands/brain.js +0 -7
  14. package/commands/brief.js +0 -2
  15. package/commands/business-sync.js +0 -1
  16. package/commands/business.js +452 -12
  17. package/commands/chat-scan.js +5 -7
  18. package/commands/clean.js +31 -2
  19. package/commands/close.js +46 -11
  20. package/commands/cloud.js +0 -1
  21. package/commands/codex-goal.js +8 -12
  22. package/commands/computer.js +6 -5
  23. package/commands/console.js +64 -11
  24. package/commands/decide.js +166 -0
  25. package/commands/deck.js +30 -12
  26. package/commands/dream.js +0 -7
  27. package/commands/drill.js +14 -25
  28. package/commands/engine.js +206 -26
  29. package/commands/experiments.js +0 -4
  30. package/commands/fleet-report.js +12 -4
  31. package/commands/game.js +261 -0
  32. package/commands/gm.js +17 -7
  33. package/commands/goal.js +0 -6
  34. package/commands/harvest.js +1 -5
  35. package/commands/improve.js +29 -8
  36. package/commands/init.js +41 -37
  37. package/commands/integrations.js +0 -7
  38. package/commands/land.js +77 -20
  39. package/commands/launchpad.js +1 -16
  40. package/commands/lesson.js +17 -7
  41. package/commands/lifecycle.js +5 -5
  42. package/commands/log.js +55 -5
  43. package/commands/loop-front.js +0 -2
  44. package/commands/loops.js +6 -5
  45. package/commands/meet.js +0 -2
  46. package/commands/member.js +622 -573
  47. package/commands/mission.js +790 -314
  48. package/commands/next.js +0 -1
  49. package/commands/now.js +0 -12
  50. package/commands/one-lap.js +8 -8
  51. package/commands/orb.js +0 -4
  52. package/commands/pack-craft.js +0 -1
  53. package/commands/pack.js +3599 -424
  54. package/commands/play.js +6 -6
  55. package/commands/probe.js +12 -6
  56. package/commands/pulse.js +88 -64
  57. package/commands/push.js +0 -4
  58. package/commands/radar.js +14 -11
  59. package/commands/recover.js +340 -0
  60. package/commands/release.js +10 -9
  61. package/commands/report.js +0 -1
  62. package/commands/router.js +320 -0
  63. package/commands/run-front.js +0 -2
  64. package/commands/search.js +0 -3
  65. package/commands/site-deploy.js +870 -0
  66. package/commands/site.js +11 -2
  67. package/commands/skill.js +7 -2
  68. package/commands/slop.js +237 -7
  69. package/commands/status.js +6 -0
  70. package/commands/stream.js +4 -20
  71. package/commands/study.js +0 -3
  72. package/commands/sync-checkout.js +51 -0
  73. package/commands/sync.js +11 -40
  74. package/commands/task.js +999 -595
  75. package/commands/taste.js +101 -0
  76. package/commands/teach.js +86 -0
  77. package/commands/team.js +83 -3
  78. package/commands/unknowns.js +0 -10
  79. package/commands/update.js +0 -1
  80. package/commands/vercel.js +4 -2
  81. package/commands/visualize.js +0 -5
  82. package/commands/voice.js +195 -0
  83. package/commands/watch.js +1 -28
  84. package/commands/wiki.js +42 -15
  85. package/commands/wish.js +0 -1
  86. package/commands/workflow.js +2 -2
  87. package/commands/worktree.js +88 -28
  88. package/commands/xp.js +28 -30
  89. package/commands/youtube.js +0 -2
  90. package/lib/accept-verify-gate.js +126 -0
  91. package/lib/activity-stream.js +0 -2
  92. package/lib/arg-parser.js +41 -0
  93. package/lib/auto-accept-certified.js +261 -3
  94. package/lib/autoland.js +66 -3
  95. package/lib/ax-auto-lane.js +68 -7
  96. package/lib/bench/context.js +0 -2
  97. package/lib/bench/report.js +0 -1
  98. package/lib/bench/runner.js +18 -11
  99. package/lib/brief-ledger.js +0 -6
  100. package/lib/business-simulate.js +382 -0
  101. package/lib/chat-log-scan.js +0 -5
  102. package/lib/checkout-sync.js +52 -0
  103. package/lib/claude-boot-block.js +48 -0
  104. package/lib/cloud-mission.js +0 -6
  105. package/lib/codex-flight.js +0 -1
  106. package/lib/conductor-artifacts.js +25 -0
  107. package/lib/context-gatherer.js +7 -2
  108. package/lib/deck-compose.js +0 -1
  109. package/lib/deck-history.js +0 -1
  110. package/lib/deck-layout.js +0 -5
  111. package/lib/deck-review.js +0 -7
  112. package/lib/deck-schema.js +0 -1
  113. package/lib/default-verifier.js +0 -2
  114. package/lib/engine-registry.js +157 -28
  115. package/lib/experiments/daily.js +0 -6
  116. package/lib/falsifier-probe.js +84 -0
  117. package/lib/fleet.js +275 -42
  118. package/lib/functional-owner.js +0 -3
  119. package/lib/git-spawn.js +15 -0
  120. package/lib/inspect-fields.js +0 -1
  121. package/lib/json-file.js +37 -0
  122. package/lib/known-commands.js +2 -2
  123. package/lib/learnings.js +0 -2
  124. package/lib/lesson-preflight.js +146 -0
  125. package/lib/loop-doctor.js +11 -19
  126. package/lib/manifest.js +0 -2
  127. package/lib/member-alive.js +0 -1
  128. package/lib/member-switches.js +0 -5
  129. package/lib/mission-artifact.js +0 -1
  130. package/lib/mission-human-asks.js +28 -0
  131. package/lib/mission-protected-lane.js +296 -0
  132. package/lib/mission-room.js +0 -2
  133. package/lib/mission-runtime-loop.js +0 -1
  134. package/lib/next-moves.js +0 -18
  135. package/lib/official-cli-integration.js +47 -6
  136. package/lib/orb-context.js +14 -4
  137. package/lib/orb-scorecard.js +0 -1
  138. package/lib/outbound-send-gate.js +0 -6
  139. package/lib/pack-capabilities.js +685 -0
  140. package/lib/permission-grants.js +0 -6
  141. package/lib/policy-lessons.js +0 -2
  142. package/lib/pulse.js +68 -15
  143. package/lib/review-integrity.js +0 -2
  144. package/lib/router-brain.js +51 -3
  145. package/lib/runner-command.js +0 -10
  146. package/lib/runtime-bootstrap.js +0 -1
  147. package/lib/scorecard.js +0 -1
  148. package/lib/self-drive.js +45 -14
  149. package/lib/state-detection.js +0 -6
  150. package/lib/task-db.js +137 -7
  151. package/lib/task-decision.js +50 -0
  152. package/lib/taste-lessons.js +153 -0
  153. package/lib/team-presence.js +0 -1
  154. package/lib/tool-result-encode.js +17 -1
  155. package/lib/voice-gate.js +129 -2
  156. package/lib/wiki.js +152 -9
  157. package/lib/wish-audit.js +21 -7
  158. package/lib/wish-delegate.js +27 -21
  159. package/lib/wish-stats.js +0 -5
  160. package/lib/wish-store.js +0 -1
  161. package/lib/workspace-safety.js +0 -2
  162. package/lib/zip.js +95 -8
  163. package/package.json +2 -1
  164. package/templates/business-starter/persona.md +9 -0
package/commands/clean.js CHANGED
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
4
  const escapeRegExp = require('../lib/escape-regexp');
5
+ const { wikiMetabolismNudge } = require('../lib/wiki');
5
6
 
6
7
  /**
7
8
  * atris clean - Workspace housekeeping with auto-heal
@@ -60,6 +61,14 @@ function cleanAtris(options = {}) {
60
61
  const stalePages = findStalePages(cwd, atrisDir);
61
62
  results.stalePages = stalePages;
62
63
 
64
+ // 6. Dead code report (report-only: deletion is a human/reviewed call)
65
+ try {
66
+ const { findDeadCode } = require('./slop');
67
+ const dead = findDeadCode(cwd);
68
+ results.deadFiles = dead.dead.map((f) => path.relative(cwd, f));
69
+ results.testOnlyFiles = dead.testOnly.map((f) => path.relative(cwd, f));
70
+ } catch { results.deadFiles = []; results.testOnlyFiles = []; }
71
+
63
72
  if (options.json) {
64
73
  console.log(JSON.stringify(cleanResultPayload(results, options, cwd), null, 2));
65
74
  return results;
@@ -75,6 +84,12 @@ function cleanAtris(options = {}) {
75
84
  console.log('Results:');
76
85
  console.log('');
77
86
 
87
+ const wikiNudge = wikiMetabolismNudge(cwd);
88
+ if (wikiNudge) {
89
+ console.log(wikiNudge);
90
+ console.log('');
91
+ }
92
+
78
93
  // Stale tasks
79
94
  if (staleTasks.length > 0) {
80
95
  console.log(`⚠ ${staleTasks.length} stale ${staleTasks.length === 1 ? 'task' : 'tasks'} (claimed >3 days, not completed):`);
@@ -140,6 +155,16 @@ function cleanAtris(options = {}) {
140
155
  console.log('✓ No stale wiki pages');
141
156
  }
142
157
 
158
+ // Dead code
159
+ if ((results.deadFiles || []).length > 0) {
160
+ console.log(`⚠ ${results.deadFiles.length} dead ${results.deadFiles.length === 1 ? 'file' : 'files'} (unreachable and unreferenced — atris slop dead for detail):`);
161
+ results.deadFiles.slice(0, 5).forEach((f) => console.log(` • ${f}`));
162
+ if (results.deadFiles.length > 5) console.log(` ... and ${results.deadFiles.length - 5} more`);
163
+ console.log('');
164
+ } else {
165
+ console.log('✓ No dead code');
166
+ }
167
+
143
168
  const hasIssues = staleTasks.length > 0 || unhealable.length > 0 || stalePages.length > 0;
144
169
  if (hasIssues) {
145
170
  console.log('Manual action needed:');
@@ -165,6 +190,7 @@ function cleanResultPayload(results, options = {}, cwd = process.cwd()) {
165
190
  if (results.staleTasks.length > 0) manualAction.push('Delete stale tasks or finish them');
166
191
  if (results.unhealableRefs.length > 0) manualAction.push('Manually fix unhealable MAP.md refs');
167
192
  if (results.stalePages.length > 0) manualAction.push('Re-compile stale pages');
193
+ if ((results.deadFiles || []).length > 0) manualAction.push('Delete dead files (atris slop dead)');
168
194
  return {
169
195
  ok: manualAction.length === 0,
170
196
  action: 'clean',
@@ -196,6 +222,11 @@ function cleanResultPayload(results, options = {}, cwd = process.cwd()) {
196
222
  stale_source: page.staleSource,
197
223
  })),
198
224
  },
225
+ dead_code: {
226
+ count: (results.deadFiles || []).length,
227
+ items: results.deadFiles || [],
228
+ test_only: results.testOnlyFiles || [],
229
+ },
199
230
  },
200
231
  manual_action: manualAction,
201
232
  };
@@ -659,8 +690,6 @@ module.exports = {
659
690
  cleanResultPayload,
660
691
  findStaleTasks,
661
692
  healBrokenMapRefs,
662
- archiveOldJournals,
663
- cleanEmptySections,
664
693
  findStalePages,
665
694
  checkPageStaleness
666
695
  };
package/commands/close.js CHANGED
@@ -517,14 +517,27 @@ function pulseLivenessState(cwd, now) {
517
517
  // pulse liveness check only sees silence; this sees a run of negative reward.
518
518
  // Sums reward over the last N finished ticks; unhealthy when the trend is net
519
519
  // negative, healthy once it climbs back to zero or better.
520
+ //
521
+ // Recency guard: this sensor only speaks to a *live* run of ticks. A
522
+ // decommissioned or intermittent loop leaves stale receipts frozen; without a
523
+ // guard the last N finished ticks stay net-negative forever and the flag can
524
+ // never clear (no fresh positive receipt arrives). It also must not treat a
525
+ // scattered set — e.g. one tick today plus five from days ago — as a "run":
526
+ // summing rewards across a multi-day gap is meaningless. So we only consider
527
+ // finished ticks from the last few days and require a full window of them; a
528
+ // silent or sparse loop is the pulse-liveness sensor's concern, and here it
529
+ // reports healthy so any open reward flag auto-closes.
520
530
  const REWARD_REGRESSION_WINDOW = 6;
531
+ const REWARD_REGRESSION_RECENCY_MS = 2 * DAY_MS;
521
532
 
522
533
  function rewardRegressionState(cwd, now) {
523
534
  const receipts = pulse.readJsonl(pulse.pulseReceiptsPath(cwd));
524
535
  const finished = receipts
525
536
  .filter((r) => r && r.phase === 'finished' && parseDate(r.ts || r.at) !== null)
537
+ .filter((r) => now.getTime() - parseDate(r.ts || r.at) <= REWARD_REGRESSION_RECENCY_MS)
526
538
  .sort((a, b) => parseDate(a.ts || a.at) - parseDate(b.ts || b.at));
527
- if (finished.length < REWARD_REGRESSION_WINDOW) return { healthy: false, unhealthy: false };
539
+ // Not enough recent signal to call a degradation run: leave it to pulse liveness.
540
+ if (finished.length < REWARD_REGRESSION_WINDOW) return { healthy: true, unhealthy: false, sparse: true };
528
541
  const window = finished.slice(-REWARD_REGRESSION_WINDOW);
529
542
  const sum = window.reduce((acc, r) => acc + (Number(r.reward) || 0), 0);
530
543
  return { healthy: sum >= 0, unhealthy: sum < 0, sum };
@@ -782,13 +795,14 @@ function appendOpenedFromSource({ what, owner = 'operator', lane = 'code', ttlDa
782
795
  return event;
783
796
  }
784
797
 
785
- function appendClosedFromSource(flag, cwd, now) {
798
+ function appendClosedFromSource(flag, cwd, now, note) {
786
799
  const event = {
787
800
  kind: 'closed',
788
801
  at: now.toISOString(),
789
802
  id: flag.id,
790
803
  proof: RESOLVED_IN_SOURCE_PROOF,
791
804
  };
805
+ if (note) event.note = note;
792
806
  appendEvent(event, cwd);
793
807
  return event;
794
808
  }
@@ -809,12 +823,20 @@ function scanState(cwd = process.cwd(), options = {}) {
809
823
  const lessonFlags = lessonFlagsState(cwd, now);
810
824
  const openBySource = new Map();
811
825
  const dissolvedSources = new Set();
826
+ const recentlyClosedSources = new Set();
812
827
  const autoClosed = [];
813
828
 
814
829
  // Dissolve is a durable verdict: a source the operator declared dead on
815
- // purpose must never re-open on a later scan.
830
+ // purpose must never re-open on a later scan. Recent closes get a 30-day
831
+ // quiet period so a resolved source cannot immediately breed a new flag.
816
832
  for (const flag of foldEvents(readEvents(cwd), { now })) {
817
833
  if (flag.status === 'dissolved' && flag.source) dissolvedSources.add(flag.source);
834
+ const closedAt = parseDate(flag.closed_at);
835
+ const closedAge = closedAt === null ? null : now.getTime() - closedAt;
836
+ if (flag.status === 'closed' && flag.source && closedAge !== null
837
+ && closedAge >= 0 && closedAge <= 30 * DAY_MS) {
838
+ recentlyClosedSources.add(flag.source);
839
+ }
818
840
  }
819
841
 
820
842
  const failedTasks = Array.from(tasksById.values())
@@ -827,17 +849,22 @@ function scanState(cwd = process.cwd(), options = {}) {
827
849
  if (flag.source) openBySource.set(flag.source, flag);
828
850
  }
829
851
 
830
- const closeFlag = (flag) => {
831
- const event = appendClosedFromSource(flag, cwd, now);
852
+ const closeFlag = (flag, note) => {
853
+ const event = appendClosedFromSource(flag, cwd, now, note);
832
854
  autoClosed.push({ flag, event });
833
855
  openBySource.delete(flag.source);
856
+ recentlyClosedSources.add(flag.source);
834
857
  };
835
858
 
836
859
  for (const flag of Array.from(openBySource.values())) {
837
860
  if (flag.source.startsWith('task:')) {
838
861
  const task = tasksById.get(flag.source.slice('task:'.length));
839
862
  const status = normalizeSpaces(task && task.status).toLowerCase();
840
- if (task && (taskResolved(task) || status === 'failed')) closeFlag(flag);
863
+ if (!task) {
864
+ closeFlag(flag, 'task no longer in projection (done/archived)');
865
+ } else if (!taskWaitingOnHumanAccept(task) || taskResolved(task) || status === 'failed') {
866
+ closeFlag(flag);
867
+ }
841
868
  continue;
842
869
  }
843
870
  if (flag.source.startsWith(FAILED_TASK_BATCH_PREFIX)) {
@@ -881,7 +908,8 @@ function scanState(cwd = process.cwd(), options = {}) {
881
908
 
882
909
  const opened = [];
883
910
  const openCandidate = (candidate) => {
884
- if (!candidate.source || openBySource.has(candidate.source) || dissolvedSources.has(candidate.source)) return null;
911
+ if (!candidate.source || openBySource.has(candidate.source)
912
+ || dissolvedSources.has(candidate.source) || recentlyClosedSources.has(candidate.source)) return null;
885
913
  const event = appendOpenedFromSource(candidate, cwd, now);
886
914
  opened.push(event);
887
915
  openBySource.set(candidate.source, { ...event, status: 'open' });
@@ -889,10 +917,11 @@ function scanState(cwd = process.cwd(), options = {}) {
889
917
  };
890
918
 
891
919
  for (const task of tasks) {
892
- const source = taskSource(task);
893
- const ref = taskRef(task);
920
+ const currentTask = tasksById.get(normalizeSpaces(task && task.id));
921
+ const source = taskSource(currentTask);
922
+ const ref = taskRef(currentTask);
894
923
  if (!source || !ref) continue;
895
- if (taskWaitingOnHumanAccept(task) && isOlderThan(task.updated_at, 5, now)) {
924
+ if (taskWaitingOnHumanAccept(currentTask) && isOlderThan(currentTask.updated_at, 5, now)) {
896
925
  openCandidate({
897
926
  what: `task ${ref} has waited in review too long, accept it or send it back`,
898
927
  lane: 'code',
@@ -1054,6 +1083,13 @@ function run(args = [], context = {}) {
1054
1083
  printHelp();
1055
1084
  return 0;
1056
1085
  }
1086
+ // `close <sub> --help` is a help request, not a run — sweep and scan mutate
1087
+ // the ledger (escalations, auto-closes), so never execute a subcommand just
1088
+ // to show usage.
1089
+ if (rest.includes('--help') || rest.includes('-h')) {
1090
+ printHelp();
1091
+ return 0;
1092
+ }
1057
1093
  if (subcommand === 'add') return commandAdd(rest, context);
1058
1094
  if (subcommand === 'list') return commandList(rest, context);
1059
1095
  if (subcommand === 'done') return commandDone(rest, context);
@@ -1078,6 +1114,5 @@ module.exports = {
1078
1114
  openFlags,
1079
1115
  ledgerPath,
1080
1116
  readEvents,
1081
- listLine,
1082
1117
  sweepLine,
1083
1118
  };
package/commands/cloud.js CHANGED
@@ -240,6 +240,5 @@ module.exports = {
240
240
  cloudClean,
241
241
  collectCloudOrphans,
242
242
  parseCloudCleanArgs,
243
- readCloudCleanSlug,
244
243
  renderCloudCleanSummary,
245
244
  };
@@ -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, [
@@ -379,7 +377,5 @@ function codexGoalCommand(args = []) {
379
377
 
380
378
  module.exports = {
381
379
  codexGoalCommand,
382
- CONFIRM_RESET_FLAG,
383
- resolveStatePath,
384
380
  sqlString,
385
381
  };
@@ -1405,7 +1405,7 @@ function computerLocalLegacy(extraArgs = []) {
1405
1405
  }
1406
1406
  }
1407
1407
 
1408
- function computerLocal(extraArgs = []) {
1408
+ function computerLocal(extraArgs = [], options = {}) {
1409
1409
  printLocalWordmark();
1410
1410
  printModeBanner('LOCAL', process.cwd(), [
1411
1411
  'Claude Code + Atris workspace context.',
@@ -1417,7 +1417,7 @@ function computerLocal(extraArgs = []) {
1417
1417
  const originalArgv = process.argv;
1418
1418
  process.argv = [originalArgv[0], originalArgv[1], originalArgv[2], 'claude', ...extraArgs];
1419
1419
  try {
1420
- consoleCommand();
1420
+ consoleCommand(options);
1421
1421
  } finally {
1422
1422
  process.argv = originalArgv;
1423
1423
  }
@@ -4390,11 +4390,12 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
4390
4390
  module.exports = {
4391
4391
  runComputer,
4392
4392
  computerSetup,
4393
- setupComputerChoices,
4393
+ // `atris pack run` starts a packet as a free local computer, so it needs the
4394
+ // local starter and the business-binding check as seams, not copies.
4395
+ computerLocal,
4396
+ readBusinessBinding,
4394
4397
  formatEngineSeats,
4395
4398
  buildComputerCard,
4396
- renderComputerCard,
4397
- renderComputerCardMarkdown,
4398
4399
  extractAttachedWorkspaceMismatch,
4399
4400
  contextForAttachedWorkspaceMismatch,
4400
4401
  printRecruitingLocalSyncOutcome,
@@ -7,10 +7,40 @@ const { resolveClaudeRunnerBin } = require('../lib/runner-command');
7
7
 
8
8
  // ── Context Gathering ──────────────────────────────────────────────
9
9
 
10
+ // Atris content lives in two shapes on disk, and the console has to read both.
11
+ //
12
+ // workspace: <dir>/atris/{skills,team,TODO.md,...}
13
+ // The normal repo shape. A project keeps its Atris brain in a subfolder so
14
+ // it does not collide with the project's own files. This is the common case.
15
+ //
16
+ // packet: <dir>/{pack.json,skills,team,docs,...}
17
+ // A packet built in root mode (pack.js exports with prefix '' when the
18
+ // source dir has pack.json at its root — this is how the shipped packs are
19
+ // built and what the web /packs/<slug>/download route serves). `pack
20
+ // install` preserves that flat layout, so the packet folder IS the atris
21
+ // root; there is no nested atris/ to find.
22
+ //
23
+ // Nested wins when both exist: never change what an existing workspace loads.
24
+ // Returns the directory that actually holds the atris content, or null.
25
+ function resolveAtrisRoot(workspaceDir) {
26
+ const nested = path.join(workspaceDir, 'atris');
27
+ try {
28
+ if (fs.statSync(nested).isDirectory()) return nested;
29
+ } catch {}
30
+ const manifest = path.join(workspaceDir, 'pack.json');
31
+ try {
32
+ if (!fs.statSync(manifest).isFile()) return null;
33
+ JSON.parse(fs.readFileSync(manifest, 'utf8'));
34
+ return workspaceDir;
35
+ } catch {}
36
+ return null;
37
+ }
38
+
10
39
  function gatherAtrisContext(workspaceDir) {
11
- const atrisDir = path.join(workspaceDir, 'atris');
40
+ const atrisDir = resolveAtrisRoot(workspaceDir);
12
41
  const ctx = {
13
- hasAtris: fs.existsSync(atrisDir),
42
+ hasAtris: Boolean(atrisDir),
43
+ atrisRoot: atrisDir,
14
44
  skills: [],
15
45
  teamMembers: [],
16
46
  backlogCount: 0,
@@ -119,7 +149,8 @@ function buildSystemPrompt(ctx) {
119
149
  if (ctx.teamMembers.length > 0) {
120
150
  lines.push('## Team Members');
121
151
  lines.push(`Available: ${ctx.teamMembers.join(', ')}`);
122
- lines.push('Each has a MEMBER.md in atris/team/<name>/ defining their role.');
152
+ const teamRoot = ctx.atrisRoot ? path.join(ctx.atrisRoot, 'team') : 'atris/team';
153
+ lines.push(`Each has a MEMBER.md in ${teamRoot}/<name>/ defining their role.`);
123
154
  lines.push('');
124
155
  }
125
156
 
@@ -135,7 +166,8 @@ function buildSystemPrompt(ctx) {
135
166
 
136
167
  if (ctx.backlogCount > 0) {
137
168
  lines.push(`## Current Work`);
138
- lines.push(`${ctx.backlogCount} task${ctx.backlogCount > 1 ? 's' : ''} in backlog. Check atris/TODO.md for details.`);
169
+ const todoPath = ctx.atrisRoot ? path.join(ctx.atrisRoot, 'TODO.md') : 'atris/TODO.md';
170
+ lines.push(`${ctx.backlogCount} task${ctx.backlogCount > 1 ? 's' : ''} in backlog. Check ${todoPath} for details.`);
139
171
  lines.push('');
140
172
  }
141
173
 
@@ -280,20 +312,37 @@ function checkAuth(backend) {
280
312
 
281
313
  // ── Launch ──────────────────────────────────────────────────────────
282
314
 
283
- function launchClaude(systemPrompt, extraArgs) {
315
+ function launchClaude(systemPrompt, extraArgs, options = {}) {
316
+ const skipPermissions = options.skipPermissions !== false;
284
317
  const runnerBin = resolveClaudeRunnerBin();
318
+ const promptStdin = typeof options.promptStdin === 'string'
319
+ ? options.promptStdin
320
+ : null;
285
321
  const args = [
286
- '--dangerously-skip-permissions',
322
+ ...(skipPermissions ? ['--dangerously-skip-permissions'] : []),
323
+ ...(promptStdin !== null ? ['--print'] : []),
287
324
  '--append-system-prompt', systemPrompt,
288
325
  ...extraArgs,
289
326
  ];
290
327
 
291
328
  const child = spawnSync(runnerBin, args, {
292
329
  cwd: process.cwd(),
293
- stdio: 'inherit',
294
- 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 },
295
333
  });
296
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
+
297
346
  if (child.error) {
298
347
  console.error(`✗ Failed to start ${runnerBin}: ${child.error.message}`);
299
348
  process.exit(1);
@@ -342,7 +391,11 @@ function launchCodex(systemPrompt, extraArgs) {
342
391
 
343
392
  // ── Main ────────────────────────────────────────────────────────────
344
393
 
345
- function consoleCommand() {
394
+ // skipPermissions defaults to true: `atris console` runs on a workspace you
395
+ // wrote. Callers that launch an agent inside content someone else authored
396
+ // (`atris pack run`) pass false so permission prompts stay on.
397
+ function consoleCommand(options = {}) {
398
+ const skipPermissions = options.skipPermissions !== false;
346
399
  const args = process.argv.slice(3);
347
400
  let requested = args[0];
348
401
 
@@ -385,7 +438,7 @@ function consoleCommand() {
385
438
 
386
439
  // Launch
387
440
  if (backend === 'claude') {
388
- launchClaude(systemPrompt, extraArgs);
441
+ launchClaude(systemPrompt, extraArgs, { ...options, skipPermissions });
389
442
  } else {
390
443
  launchCodex(systemPrompt, extraArgs);
391
444
  }
@@ -399,4 +452,4 @@ function consoleCommand() {
399
452
  }
400
453
  }
401
454
 
402
- module.exports = { consoleCommand, gatherAtrisContext, buildSystemPrompt };
455
+ module.exports = { consoleCommand, gatherAtrisContext, buildSystemPrompt, resolveAtrisRoot, launchClaude };
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ answerMissionHumanAsk,
5
+ listMissions,
6
+ listWorktreeRollupMissions,
7
+ pingMission,
8
+ } = require('./mission');
9
+ const { openHumanAsks, normalizeHumanAsks } = require('../lib/mission-human-asks');
10
+ const { redirectToWorkspaceRoot } = require('../lib/mission-root');
11
+ const { shortId } = require('../lib/short-name');
12
+
13
+ const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
14
+
15
+ function missionTouchedAt(mission) {
16
+ return String(mission.updated_at || mission.created_at || '');
17
+ }
18
+
19
+ function liveMissions(root = process.cwd()) {
20
+ const seen = new Set();
21
+ return [...listMissions(root), ...listWorktreeRollupMissions(root)]
22
+ .filter((mission) => {
23
+ if (!mission || !mission.id || seen.has(mission.id) || TERMINAL_STATUSES.has(mission.status)) return false;
24
+ seen.add(mission.id);
25
+ return true;
26
+ })
27
+ .sort((left, right) => (
28
+ missionTouchedAt(right).localeCompare(missionTouchedAt(left))
29
+ || String(left.id).localeCompare(String(right.id))
30
+ ));
31
+ }
32
+
33
+ function collectOpenDecisions(root = process.cwd()) {
34
+ const decisions = [];
35
+ for (const mission of liveMissions(root)) {
36
+ const normalized = normalizeHumanAsks(mission.human_asks);
37
+ normalized.forEach((ask, askIndex) => {
38
+ if (!ask.text.trim() || ask.answered_at) return;
39
+ decisions.push({
40
+ number: decisions.length + 1,
41
+ owner: String(mission.owner || 'unowned'),
42
+ mission_id: mission.id,
43
+ mission_short_id: shortId(mission.id),
44
+ mission_status: mission.status,
45
+ mission_updated_at: missionTouchedAt(mission),
46
+ ask_index: askIndex,
47
+ text: ask.text,
48
+ });
49
+ });
50
+ }
51
+ return decisions;
52
+ }
53
+
54
+ function printHelp() {
55
+ console.log('Usage:');
56
+ console.log(' atris decide');
57
+ console.log(' atris decide <n> y|n|yes|no [--note "<text>"]');
58
+ console.log(' atris decide --json');
59
+ console.log(' atris decide <n> y --json');
60
+ }
61
+
62
+ function fail(message, asJson, code = 2) {
63
+ if (asJson) {
64
+ console.log(JSON.stringify({ ok: false, action: 'decide_error', error: message }));
65
+ } else {
66
+ console.error(message);
67
+ }
68
+ process.exitCode = code;
69
+ }
70
+
71
+ function parseArgs(args) {
72
+ const asJson = args.includes('--json');
73
+ const rest = args.filter((arg) => arg !== '--json');
74
+ let note = '';
75
+ const noteIndex = rest.findIndex((arg) => arg === '--note' || String(arg).startsWith('--note='));
76
+ if (noteIndex !== -1) {
77
+ const noteArg = String(rest[noteIndex]);
78
+ if (noteArg === '--note') {
79
+ if (rest[noteIndex + 1] == null) return { asJson, error: '--note requires text' };
80
+ note = String(rest[noteIndex + 1]).trim();
81
+ rest.splice(noteIndex, 2);
82
+ } else {
83
+ note = noteArg.slice('--note='.length).trim();
84
+ rest.splice(noteIndex, 1);
85
+ }
86
+ }
87
+ return { asJson, note, rest };
88
+ }
89
+
90
+ function decideCommand(args = []) {
91
+ redirectToWorkspaceRoot();
92
+ const parsed = parseArgs(args);
93
+ if (parsed.error) return fail(parsed.error, parsed.asJson);
94
+ const { asJson, note, rest } = parsed;
95
+ if (rest.includes('--help') || rest.includes('-h') || rest[0] === 'help') {
96
+ printHelp();
97
+ return;
98
+ }
99
+
100
+ const decisions = collectOpenDecisions();
101
+ if (!rest.length) {
102
+ if (asJson) {
103
+ console.log(JSON.stringify({
104
+ ok: true,
105
+ action: 'decide_list',
106
+ count: decisions.length,
107
+ decisions,
108
+ }, null, 2));
109
+ } else if (!decisions.length) {
110
+ console.log('nothing is waiting for a decision.');
111
+ } else {
112
+ for (const decision of decisions) {
113
+ console.log(`[${decision.number}] ${decision.owner} · ${decision.mission_short_id} · ${decision.text}`);
114
+ }
115
+ console.log('atris decide <n> y|n');
116
+ }
117
+ return;
118
+ }
119
+
120
+ if (rest.length !== 2) {
121
+ return fail('usage: atris decide <n> y|n|yes|no [--note "<text>"]', asJson);
122
+ }
123
+ const number = Number(rest[0]);
124
+ if (!Number.isInteger(number) || number < 1) {
125
+ return fail('decision number must be a positive integer', asJson);
126
+ }
127
+ const answerToken = String(rest[1]).toLowerCase();
128
+ const answer = answerToken === 'y' || answerToken === 'yes'
129
+ ? 'yes'
130
+ : (answerToken === 'n' || answerToken === 'no' ? 'no' : null);
131
+ if (!answer) return fail('answer must be y, n, yes, or no', asJson);
132
+ const decision = decisions[number - 1];
133
+ if (!decision) return fail(`decision ${number} is not open`, asJson, 1);
134
+
135
+ const message = `Decision on "${decision.text}": ${answer.toUpperCase()}${note ? ` — ${note}` : ''}`;
136
+ pingMission([decision.mission_id, message, '--from', 'decide'], { silent: true });
137
+ const mission = answerMissionHumanAsk(decision.mission_id, decision.ask_index, answer, note);
138
+ const remainingOpenAsks = openHumanAsks(mission.human_asks).length;
139
+ const payload = {
140
+ ok: true,
141
+ action: 'decision_answered',
142
+ decision: {
143
+ ...decision,
144
+ answer,
145
+ note,
146
+ message,
147
+ },
148
+ mission: {
149
+ id: mission.id,
150
+ short_id: decision.mission_short_id,
151
+ owner: mission.owner,
152
+ status: mission.status,
153
+ remaining_open_asks: remainingOpenAsks,
154
+ },
155
+ };
156
+ if (asJson) {
157
+ console.log(JSON.stringify(payload, null, 2));
158
+ } else {
159
+ console.log(`sent to ${decision.mission_short_id}: ${message}`);
160
+ console.log(`mission ${decision.mission_short_id} will read it on its next tick.`);
161
+ }
162
+ }
163
+
164
+ module.exports = {
165
+ decideCommand,
166
+ };