atris 3.52.0 → 3.55.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 (131) hide show
  1. package/FOR_AGENTS.md +7 -0
  2. package/README.md +1 -1
  3. package/atris/policies/ANTISLOP.md +2 -1
  4. package/atris/skills/copy-editor/SKILL.md +2 -1
  5. package/atris/skills/design/SKILL.md +1 -1
  6. package/atris/skills/engines/SKILL.md +8 -7
  7. package/atris/skills/x-search/SKILL.md +24 -22
  8. package/atris/skills/youtube/SKILL.md +89 -40
  9. package/atris/team/_template/SOUL.md +40 -0
  10. package/atris/team/validator/MEMBER.md +1 -2
  11. package/ax +592 -91
  12. package/bin/atris +21 -0
  13. package/bin/atris.js +580 -378
  14. package/commands/api-key.js +170 -0
  15. package/commands/auth.js +453 -43
  16. package/commands/autoland.js +3 -1
  17. package/commands/autopilot-front.js +58 -11
  18. package/commands/avail.js +5 -4
  19. package/commands/balance.js +55 -0
  20. package/commands/bench.js +29 -1
  21. package/commands/brain.js +63 -12
  22. package/commands/brainstorm.js +18 -0
  23. package/commands/business.js +149 -1
  24. package/commands/chat-scan.js +17 -4
  25. package/commands/close.js +5 -1
  26. package/commands/computer.js +2 -2
  27. package/commands/doctor.js +174 -0
  28. package/commands/dream.js +7 -0
  29. package/commands/drill.js +34 -2
  30. package/commands/engine.js +47 -35
  31. package/commands/errors.js +58 -8
  32. package/commands/experiments.js +14 -0
  33. package/commands/feedback.js +61 -2
  34. package/commands/fleet-report.js +27 -7
  35. package/commands/fleet.js +98 -11
  36. package/commands/founder.js +60 -21
  37. package/commands/human-missions.js +79 -4
  38. package/commands/improve.js +54 -4
  39. package/commands/init.js +68 -92
  40. package/commands/install.js +40 -0
  41. package/commands/integrations.js +399 -30
  42. package/commands/interview.js +8 -0
  43. package/commands/land.js +48 -17
  44. package/commands/learn.js +29 -4
  45. package/commands/log.js +51 -0
  46. package/commands/member.js +68 -10
  47. package/commands/mission.js +339 -74
  48. package/commands/now.js +25 -6
  49. package/commands/orb.js +97 -2
  50. package/commands/pack.js +83 -9
  51. package/commands/playbook.js +381 -0
  52. package/commands/plugin.js +16 -0
  53. package/commands/radar.js +96 -40
  54. package/commands/recap.js +128 -21
  55. package/commands/release.js +102 -2
  56. package/commands/review.js +5 -10
  57. package/commands/revisions.js +55 -0
  58. package/commands/run-front.js +12 -4
  59. package/commands/scout.js +12 -1
  60. package/commands/search.js +123 -28
  61. package/commands/sign.js +2 -2
  62. package/commands/signup.js +12 -3
  63. package/commands/site-deploy.js +17 -4
  64. package/commands/site.js +2 -2
  65. package/commands/skill-eval.js +289 -0
  66. package/commands/skill.js +56 -6
  67. package/commands/social.js +603 -0
  68. package/commands/spaceship.js +69 -6
  69. package/commands/stream.js +8 -1
  70. package/commands/study.js +32 -19
  71. package/commands/sync.js +28 -22
  72. package/commands/task.js +418 -60
  73. package/commands/teach.js +72 -63
  74. package/commands/terminal.js +25 -9
  75. package/commands/topup.js +88 -0
  76. package/commands/truth.js +52 -6
  77. package/commands/usage.js +91 -0
  78. package/commands/verify.js +3 -2
  79. package/commands/version.js +3 -1
  80. package/commands/voice.js +5 -1
  81. package/commands/who.js +69 -11
  82. package/commands/wiki.js +7 -4
  83. package/commands/wish.js +76 -5
  84. package/commands/workflow.js +327 -145
  85. package/commands/worktree.js +1 -0
  86. package/commands/write.js +5 -0
  87. package/commands/x-search.js +340 -0
  88. package/commands/xp.js +5 -1
  89. package/commands/youtube.js +551 -33
  90. package/lib/account-bound.js +47 -0
  91. package/lib/auto-accept-certified.js +10 -2
  92. package/lib/auto-lessons.js +186 -0
  93. package/lib/autoland.js +4 -2
  94. package/lib/chat-log-scan.js +7 -4
  95. package/lib/cli-json.js +77 -0
  96. package/lib/cli-scope.js +26 -0
  97. package/lib/codex-flight.js +1 -0
  98. package/lib/conductor-artifacts.js +1 -1
  99. package/lib/context-gatherer.js +11 -0
  100. package/lib/developer-api.js +116 -0
  101. package/lib/engine-ask.js +11 -3
  102. package/lib/engine-registry.js +32 -7
  103. package/lib/feedback-triage.js +274 -0
  104. package/lib/first-minute.js +398 -0
  105. package/lib/fleet.js +115 -34
  106. package/lib/functional-owner.js +22 -0
  107. package/lib/known-commands.js +31 -5
  108. package/lib/member-scaffold.js +197 -0
  109. package/lib/mission-ledger-compact.js +127 -0
  110. package/lib/mission-root.js +4 -2
  111. package/lib/mission-runtime-loop.js +30 -2
  112. package/lib/next-moves.js +34 -34
  113. package/lib/noninteractive.js +88 -0
  114. package/lib/permission-grants.js +10 -1
  115. package/lib/policy-lessons.js +4 -1
  116. package/lib/revision-metric.js +279 -0
  117. package/lib/runner-command.js +5 -4
  118. package/lib/scratch-root.js +48 -0
  119. package/lib/skill-eval-gate.js +249 -0
  120. package/lib/task-db.js +22 -2
  121. package/lib/task-proof.js +75 -9
  122. package/lib/task-receipt.js +5 -1
  123. package/lib/verifier-quality.js +69 -0
  124. package/lib/wish-audit.js +2 -2
  125. package/lib/wish-delegate.js +10 -1
  126. package/lib/workspace-scaffold.js +270 -0
  127. package/package.json +4 -2
  128. package/scripts/det/checklist-score.js +191 -0
  129. package/scripts/det/ytsearch +31 -0
  130. package/scripts/outbound-artifact-gate.js +227 -0
  131. package/utils/update-check.js +15 -0
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const ACCOUNT_GLOBAL_MESSAGE = 'account-global; pass --account to continue';
7
+
8
+ function isBoundBusinessWorkspace(cwd = process.cwd()) {
9
+ const root = path.resolve(cwd);
10
+ return fs.existsSync(path.join(root, '.atris', 'business.json'))
11
+ && fs.existsSync(path.join(root, 'atris'));
12
+ }
13
+
14
+ function isHelpArg(arg) {
15
+ return arg === '--help' || arg === '-h' || arg === 'help';
16
+ }
17
+
18
+ /**
19
+ * Account-scoped verbs dump live CRM/cloud state. Outside a bound business
20
+ * workspace they must opt in with --account. Help always passes.
21
+ * Returns { ok, args } where args has --account stripped when continuing.
22
+ */
23
+ function requireAccountBound(args = [], options = {}) {
24
+ const list = Array.isArray(args) ? [...args] : [];
25
+ const cwd = options.cwd || process.cwd();
26
+ if (list.some(isHelpArg)) {
27
+ return { ok: true, args: list, help: true };
28
+ }
29
+ if (isBoundBusinessWorkspace(cwd)) {
30
+ return { ok: true, args: list.filter((arg) => arg !== '--account'), bound: true };
31
+ }
32
+ if (list.includes('--account')) {
33
+ return { ok: true, args: list.filter((arg) => arg !== '--account'), account: true };
34
+ }
35
+ return { ok: false, args: list, message: ACCOUNT_GLOBAL_MESSAGE };
36
+ }
37
+
38
+ function refuseAccountGlobal(write = console.error) {
39
+ write(ACCOUNT_GLOBAL_MESSAGE);
40
+ return 2;
41
+ }
42
+
43
+ module.exports = {
44
+ ACCOUNT_GLOBAL_MESSAGE,
45
+ requireAccountBound,
46
+ refuseAccountGlobal,
47
+ };
@@ -8,6 +8,7 @@ const { extractReceiptEvidence } = require('./receipt-evidence');
8
8
  const reviewIntegrity = require('./review-integrity');
9
9
  const { computeTrustTier } = require('./trust-tiers');
10
10
  const { policyLessonsForFiles, readPolicyLessons } = require('./policy-lessons');
11
+ const { skillEvalGate } = require('./skill-eval-gate');
11
12
 
12
13
  const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
13
14
  // Kept for compat with older callers/tests; the pass-count landing lane it
@@ -575,6 +576,11 @@ const repoHygieneCache = new Map();
575
576
 
576
577
  function repoHygieneGate(workspaceRoot) {
577
578
  const root = path.resolve(workspaceRoot || process.cwd());
579
+ // Skill receipts are mutable workspace state, so recompute this gate before
580
+ // consulting the repo-wide dead-code cache. A new receipt can unblock a
581
+ // landing, while a later SKILL.md edit must make an older receipt stale.
582
+ const skillEval = skillEvalGate(root);
583
+ if (!skillEval.ok) return skillEval;
578
584
  if (repoHygieneCache.has(root)) return repoHygieneCache.get(root);
579
585
  let result = { ok: true, skipped: true };
580
586
  if (fs.existsSync(path.join(root, 'test', 'repo-hygiene.test.js'))) {
@@ -617,7 +623,8 @@ function hygieneBlockResult(task, ref) {
617
623
  reason: hygiene.reason,
618
624
  message: hygiene.message,
619
625
  offenders: hygiene.offenders,
620
- next_action: 'run `node --test test/repo-hygiene.test.js` in the workspace, delete what it names, then re-certify',
626
+ next_action: hygiene.next_action
627
+ || 'run `node --test test/repo-hygiene.test.js` in the workspace, delete what it names, then re-certify',
621
628
  };
622
629
  }
623
630
 
@@ -1099,7 +1106,8 @@ function evaluateAutoAccept(task, options = {}) {
1099
1106
  if ((tierRequiresStrictVerify && !verifyResult.ok)
1100
1107
  || verifyResult.reason === 'verify_failed'
1101
1108
  || verifyResult.reason === 'verify_unrunnable'
1102
- || verifyResult.reason === 'verify_worktree_missing') {
1109
+ || verifyResult.reason === 'verify_worktree_missing'
1110
+ || verifyResult.reason === 'verify_workdir_missing') {
1103
1111
  return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
1104
1112
  }
1105
1113
  }
@@ -0,0 +1,186 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
6
+ const TYPED_LESSONS_FILE = path.join('atris', 'lessons.json');
7
+ const REPEAT_THRESHOLD = 2;
8
+ const DETECTOR = 'repeated_human_revision_note';
9
+
10
+ class AutoLessonsError extends Error {
11
+ constructor(message, cause) {
12
+ super(message, { cause });
13
+ this.name = 'AutoLessonsError';
14
+ this.code = 'AUTO_LESSONS_ERROR';
15
+ }
16
+ }
17
+
18
+ function compactText(value) {
19
+ return String(value || '').replace(/\s+/g, ' ').trim();
20
+ }
21
+
22
+ function normalizeCorrection(value) {
23
+ return compactText(value)
24
+ .normalize('NFKC')
25
+ .toLowerCase()
26
+ .replace(/[.!?]+$/, '');
27
+ }
28
+
29
+ function lessonSlug(value) {
30
+ const normalized = normalizeCorrection(value);
31
+ const slug = normalized
32
+ .normalize('NFKD')
33
+ .replace(/[\u0300-\u036f]/g, '')
34
+ .replace(/[^a-z0-9]+/g, '-')
35
+ .replace(/^-+|-+$/g, '');
36
+ if (!slug) {
37
+ return `correction-${crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 12)}`;
38
+ }
39
+ if (slug.length <= 80) return slug;
40
+ const suffix = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 8);
41
+ return `${slug.slice(0, 71).replace(/-+$/g, '')}-${suffix}`;
42
+ }
43
+
44
+ function revisionNote(episode) {
45
+ if (!episode || typeof episode !== 'object') return '';
46
+ if (episode.action?.event_type !== 'revision_requested') return '';
47
+ if (!['rework_requested', 'revised'].includes(episode.rl?.label)) return '';
48
+ return compactText(episode.human_feedback?.human_revision_note);
49
+ }
50
+
51
+ function detectRepeatSignals(episodes, options = {}) {
52
+ const threshold = Number.isInteger(options.threshold) && options.threshold > 1
53
+ ? options.threshold
54
+ : REPEAT_THRESHOLD;
55
+ const groups = new Map();
56
+ const seenEpisodes = new Set();
57
+
58
+ for (const [index, episode] of (Array.isArray(episodes) ? episodes : []).entries()) {
59
+ const note = revisionNote(episode);
60
+ if (!note) continue;
61
+ const episodeId = compactText(episode.episode_id);
62
+ if (episodeId && seenEpisodes.has(episodeId)) continue;
63
+ if (episodeId) seenEpisodes.add(episodeId);
64
+
65
+ const normalized = normalizeCorrection(note);
66
+ const slug = lessonSlug(normalized);
67
+ if (!groups.has(slug)) {
68
+ groups.set(slug, {
69
+ slug,
70
+ note,
71
+ normalized,
72
+ occurrences: [],
73
+ });
74
+ }
75
+ groups.get(slug).occurrences.push({
76
+ episode_id: episodeId || null,
77
+ task_id: compactText(episode.task_id) || null,
78
+ created_at: compactText(episode.created_at) || null,
79
+ row: index + 1,
80
+ });
81
+ }
82
+
83
+ return [...groups.values()].filter((signal) => signal.occurrences.length >= threshold);
84
+ }
85
+
86
+ function readJsonl(filePath) {
87
+ let raw;
88
+ try {
89
+ raw = fs.readFileSync(filePath, 'utf8');
90
+ } catch (error) {
91
+ if (error && error.code === 'ENOENT') return [];
92
+ throw new AutoLessonsError(`cannot read automatic lesson signals from ${filePath}: ${error.message}`, error);
93
+ }
94
+ return raw.split('\n').map((line) => {
95
+ const text = line.trim();
96
+ if (!text) return null;
97
+ try { return JSON.parse(text); } catch { return null; }
98
+ }).filter(Boolean);
99
+ }
100
+
101
+ function readTypedLessons(filePath) {
102
+ let raw;
103
+ try {
104
+ raw = fs.readFileSync(filePath, 'utf8');
105
+ } catch (error) {
106
+ if (error && error.code === 'ENOENT') return {};
107
+ throw new AutoLessonsError(`cannot read ${filePath}: ${error.message}`, error);
108
+ }
109
+
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(raw);
113
+ } catch (error) {
114
+ throw new AutoLessonsError(`cannot file automatic lessons because ${filePath} contains malformed JSON: ${error.message}`, error);
115
+ }
116
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
117
+ throw new AutoLessonsError(`cannot file automatic lessons because ${filePath} must contain a JSON object`);
118
+ }
119
+ return parsed;
120
+ }
121
+
122
+ function writeJsonAtomic(filePath, value) {
123
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
124
+ const temporaryPath = `${filePath}.auto-lessons-${process.pid}-${Date.now()}`;
125
+ try {
126
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
127
+ fs.renameSync(temporaryPath, filePath);
128
+ } catch (error) {
129
+ try { fs.rmSync(temporaryPath, { force: true }); } catch {}
130
+ throw new AutoLessonsError(`cannot write automatic lessons to ${filePath}: ${error.message}`, error);
131
+ }
132
+ }
133
+
134
+ function isoDate(now) {
135
+ const date = now instanceof Date ? now : new Date(now || Date.now());
136
+ if (Number.isNaN(date.getTime())) throw new AutoLessonsError('cannot file automatic lessons with an invalid date');
137
+ return date.toISOString().slice(0, 10);
138
+ }
139
+
140
+ function typedLesson(signal, options = {}) {
141
+ const taskIds = [...new Set(signal.occurrences.map((item) => item.task_id).filter(Boolean))];
142
+ const episodeIds = [...new Set(signal.occurrences.map((item) => item.episode_id).filter(Boolean))];
143
+ return {
144
+ name: signal.note,
145
+ date: isoDate(options.now),
146
+ scope: compactText(options.scope) || 'workspace',
147
+ rule: signal.note,
148
+ detector: DETECTOR,
149
+ source_signal: {
150
+ type: DETECTOR,
151
+ path: TASK_EPISODES_FILE,
152
+ normalized_correction: signal.normalized,
153
+ occurrences: signal.occurrences.length,
154
+ task_ids: taskIds,
155
+ episode_ids: episodeIds,
156
+ },
157
+ status: 'observed',
158
+ last_detected: isoDate(options.now),
159
+ };
160
+ }
161
+
162
+ function fileAutoLessons(root, options = {}) {
163
+ const episodes = Array.isArray(options.episodes)
164
+ ? options.episodes
165
+ : readJsonl(path.join(root, TASK_EPISODES_FILE));
166
+ const signals = detectRepeatSignals(episodes, options);
167
+ if (!signals.length) return { added: [], detected: [] };
168
+
169
+ const lessonsPath = path.join(root, TYPED_LESSONS_FILE);
170
+ const lessons = readTypedLessons(lessonsPath);
171
+ const added = [];
172
+ for (const signal of signals) {
173
+ if (Object.prototype.hasOwnProperty.call(lessons, signal.slug)) continue;
174
+ lessons[signal.slug] = typedLesson(signal, options);
175
+ added.push(signal.slug);
176
+ }
177
+ if (added.length) writeJsonAtomic(lessonsPath, lessons);
178
+ return { added, detected: signals.map((signal) => signal.slug) };
179
+ }
180
+
181
+ module.exports = {
182
+ TASK_EPISODES_FILE,
183
+ TYPED_LESSONS_FILE,
184
+ fileAutoLessons,
185
+ lessonSlug,
186
+ };
package/lib/autoland.js CHANGED
@@ -430,7 +430,7 @@ function hasAgentJargon(text) {
430
430
  return AGENT_JARGON.test(String(text || ''));
431
431
  }
432
432
 
433
- function explainResult(text) {
433
+ function explainResult(text, options = {}) {
434
434
  const raw = String(text || '');
435
435
  const value = raw.replace(/\s+/g, ' ').trim();
436
436
  if (!value) return { ok: false, reason: 'result must say what someone can do now and why it matters.' };
@@ -443,7 +443,9 @@ function explainResult(text) {
443
443
  if (RESULT_ULID.test(value)) return { ok: false, reason: 'result should explain the gain, not include database ids.' };
444
444
  if (RESULT_TICKET.test(value)) return { ok: false, reason: 'result should not include ticket ids.' };
445
445
  if (RESULT_FILE_PATH.test(value)) return { ok: false, reason: 'result should not include file paths.' };
446
- if (RESULT_COMMAND.test(value)) return { ok: false, reason: 'result should not include command syntax or flags.' };
446
+ if (!options.allowCommandMention && RESULT_COMMAND.test(value)) {
447
+ return { ok: false, reason: 'result should not include command syntax or flags.' };
448
+ }
447
449
  const finalWord = (value.replace(/[.!?]+$/g, '').match(/[A-Za-z]+$/) || [''])[0].toLowerCase();
448
450
  if (TRAILING_FRAGMENT_WORDS.has(finalWord) && !COMPLETE_TRAILING_PHRASE.test(value)) {
449
451
  return { ok: false, reason: 'result ends mid-thought; finish the sentence with the actual human gain.' };
@@ -68,12 +68,14 @@ function cursorTranscriptRoots(cwd = process.cwd()) {
68
68
  return roots;
69
69
  }
70
70
 
71
- function axLogDirs(cwd = process.cwd()) {
71
+ function axLogDirs(cwd = process.cwd(), options = {}) {
72
72
  const dirs = [];
73
73
  const local = path.join(cwd, 'atris', 'runs');
74
- const home = path.join(os.homedir(), '.atris', 'runs');
75
74
  if (fs.existsSync(local)) dirs.push(local);
76
- if (fs.existsSync(home) && home !== local) dirs.push(home);
75
+ if (options.global) {
76
+ const home = path.join(os.homedir(), '.atris', 'runs');
77
+ if (fs.existsSync(home) && home !== local) dirs.push(home);
78
+ }
77
79
  return dirs;
78
80
  }
79
81
 
@@ -276,7 +278,7 @@ function scanChatLogs(options = {}) {
276
278
  const maxAgeMs = maxAgeHours * 3600000;
277
279
  const sessions = [];
278
280
 
279
- for (const dir of axLogDirs(cwd)) {
281
+ for (const dir of axLogDirs(cwd, { global: options.global === true })) {
280
282
  for (const file of listRecentFiles(dir, { pattern: /^ax-.*\.log$/, limit, maxAgeMs })) {
281
283
  const text = safeRead(file.path);
282
284
  if (!text) continue;
@@ -304,6 +306,7 @@ function scanChatLogs(options = {}) {
304
306
  schema: SCHEMA,
305
307
  scanned_at: new Date().toISOString(),
306
308
  cwd,
309
+ scope: options.global === true ? 'global' : 'workspace',
307
310
  window_hours: maxAgeHours,
308
311
  sources: {
309
312
  ax_dirs: axLogDirs(cwd),
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+
5
+ /**
6
+ * Compact --json helpers for headless agents.
7
+ * Default payloads stay tiny; --full dumps the full object.
8
+ */
9
+
10
+ function writeStdout(text) {
11
+ const buffer = Buffer.from(`${String(text).replace(/\n?$/, '\n')}`, 'utf8');
12
+ const retryWait = new Int32Array(new SharedArrayBuffer(4));
13
+ let offset = 0;
14
+ while (offset < buffer.length) {
15
+ try {
16
+ offset += fs.writeSync(process.stdout.fd, buffer, offset, buffer.length - offset);
17
+ } catch (err) {
18
+ if (err && err.code === 'EAGAIN') {
19
+ Atomics.wait(retryWait, 0, 0, 10);
20
+ continue;
21
+ }
22
+ throw err;
23
+ }
24
+ }
25
+ }
26
+
27
+ function wantsJson(args = []) {
28
+ return Array.isArray(args) && args.includes('--json');
29
+ }
30
+
31
+ function wantsFull(args = []) {
32
+ return Array.isArray(args) && (args.includes('--full') || args.includes('--verbose'));
33
+ }
34
+
35
+ function compactErrorPayload({
36
+ reason = 'error',
37
+ detail = null,
38
+ selected_ref = null,
39
+ next_command = null,
40
+ } = {}) {
41
+ return {
42
+ ok: false,
43
+ reason: String(reason || 'error'),
44
+ detail: detail == null ? null : String(detail),
45
+ selected_ref: selected_ref == null ? null : String(selected_ref),
46
+ next_command: next_command == null ? null : String(next_command),
47
+ };
48
+ }
49
+
50
+ function compactSuccessPayload({
51
+ action,
52
+ ids = {},
53
+ next_command = null,
54
+ } = {}) {
55
+ const payload = { ok: true, action: String(action || 'ok') };
56
+ for (const [key, value] of Object.entries(ids || {})) {
57
+ if (value == null || value === '') continue;
58
+ payload[key] = value;
59
+ }
60
+ if (next_command != null && next_command !== '') {
61
+ payload.next_command = String(next_command);
62
+ }
63
+ return payload;
64
+ }
65
+
66
+ function printCliJson(fullPayload, compactPayload, args = [], print = writeStdout) {
67
+ const body = wantsFull(args) ? fullPayload : compactPayload;
68
+ print(JSON.stringify(body, null, 2));
69
+ return body;
70
+ }
71
+
72
+ module.exports = {
73
+ wantsJson,
74
+ compactErrorPayload,
75
+ compactSuccessPayload,
76
+ printCliJson,
77
+ };
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ function parseScopeFlag(args = []) {
6
+ const list = Array.isArray(args) ? args : [];
7
+ const global = list.includes('--global') || list.includes('-g');
8
+ return {
9
+ kind: global ? 'global' : 'workspace',
10
+ global,
11
+ args: list.filter((arg) => arg !== '--global' && arg !== '-g'),
12
+ };
13
+ }
14
+
15
+ function pathUnderRoot(candidate, root) {
16
+ if (!candidate || !root) return false;
17
+ const resolvedCandidate = path.resolve(String(candidate));
18
+ const resolvedRoot = path.resolve(String(root));
19
+ return resolvedCandidate === resolvedRoot
20
+ || resolvedCandidate.startsWith(`${resolvedRoot}${path.sep}`);
21
+ }
22
+
23
+ module.exports = {
24
+ parseScopeFlag,
25
+ pathUnderRoot,
26
+ };
@@ -57,6 +57,7 @@ function buildPrompt({ worktreePath, branch, brief, verifyCmd }) {
57
57
  '',
58
58
  'Instructions:',
59
59
  `- First run: atris worktree guard`,
60
+ '- Keep one concern per PR; split anything larger into separate PRs because git history guides future agents and small PRs are cheap to revert and bisect.',
60
61
  `- Verify with this bare command and use its real exit code: ${verify}`,
61
62
  '- Stage and commit only files you changed.',
62
63
  `- Commit with a plain-English message and include this trailer: ${COAUTHOR}`,
@@ -6,7 +6,7 @@
6
6
  // this list; keeping it in one place is what stops an engine from being blocked
7
7
  // by its own prompt file.
8
8
  const CONDUCTOR_UNTRACKED_PATTERN =
9
- /^\.atris\/(?:agent-worktree\.json|fleet-prompt-[^/]+\.md|codex-watchdog-[^/]+\.json|runtime-tmp(?:\/.*)?|state\/briefs\.jsonl)$/;
9
+ /^\.atris\/(?:agent-worktree\.json|fleet-prompt-[^/]+\.md|codex-watchdog\.js|codex-watchdog-[^/]+\.json|runtime-tmp(?:\/.*)?|state\/briefs\.jsonl)$/;
10
10
 
11
11
  function isConductorArtifact(file) {
12
12
  return CONDUCTOR_UNTRACKED_PATTERN.test(String(file || ''));
@@ -45,6 +45,15 @@ function inferDomain(answer) {
45
45
  return 'general';
46
46
  }
47
47
 
48
+ function isFlagLikeAnswer(answer) {
49
+ const tokens = String(answer || '').trim().split(/\s+/).filter(Boolean);
50
+ if (!tokens.length) return true;
51
+ const helpFlag = (token) => token === '--help' || token === '-h' || token === '-?';
52
+ if (tokens.some(helpFlag)) return true;
53
+ if (tokens.length === 1 && (tokens[0] === 'help' || tokens[0].startsWith('-'))) return true;
54
+ return tokens.every((token) => token.startsWith('-') || token === 'help');
55
+ }
56
+
48
57
  function starterTaskTitle(answer) {
49
58
  const summary = compactText(answer, 80) || 'first useful path';
50
59
  return `First useful step: ${summary}`;
@@ -69,6 +78,7 @@ function saveContextProfile(root, answer, { source = 'first_contact' } = {}) {
69
78
  }
70
79
 
71
80
  function createStarterTask(root, answer) {
81
+ if (isFlagLikeAnswer(answer)) return null;
72
82
  const atrisDir = path.join(root, 'atris');
73
83
  if (!fs.existsSync(atrisDir)) return null;
74
84
  try {
@@ -180,4 +190,5 @@ module.exports = {
180
190
  starterTaskTitle,
181
191
  inferDomain,
182
192
  isAtrisMetaQuestion,
193
+ isFlagLikeAnswer,
183
194
  };
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const ROLLOUT_MESSAGE = 'backend rolling out, try again shortly';
4
+ const NOT_LOGGED_IN = 'not logged in. run: atris login';
5
+
6
+ function wantsJson(args = []) {
7
+ return Array.isArray(args) && args.includes('--json');
8
+ }
9
+
10
+ function wantsHelp(args = []) {
11
+ if (!Array.isArray(args) || args.length === 0) return false;
12
+ return args.includes('--help') || args.includes('-h') || args[0] === 'help';
13
+ }
14
+
15
+ function tokenFrom(creds) {
16
+ if (!creds || typeof creds !== 'object') return '';
17
+ const token = typeof creds.token === 'string' ? creds.token.trim() : '';
18
+ return token;
19
+ }
20
+
21
+ function isNotFound(result) {
22
+ return Boolean(result && Number(result.status) === 404);
23
+ }
24
+
25
+ function oneLine(value) {
26
+ if (value && typeof value === 'object') {
27
+ const detail = value.detail || value.error || value.message;
28
+ if (detail) return oneLine(detail);
29
+ try {
30
+ return JSON.stringify(value).replace(/\s+/g, ' ').trim();
31
+ } catch {
32
+ return 'request failed';
33
+ }
34
+ }
35
+ const text = String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
36
+ return text || 'request failed';
37
+ }
38
+
39
+ function printResult(result, io = {}) {
40
+ const log = io.log || console.log;
41
+ const err = io.err || console.error;
42
+ if (result.json) {
43
+ const payload = result.payload != null
44
+ ? result.payload
45
+ : { ok: false, error: result.error || result.text || 'request failed' };
46
+ log(JSON.stringify(payload));
47
+ return;
48
+ }
49
+ if (result.ok) log(result.text);
50
+ else err(result.error || result.text || 'request failed');
51
+ }
52
+
53
+ function creditsToDollars(credits) {
54
+ const n = Number(credits);
55
+ if (!Number.isFinite(n)) return 0;
56
+ return Math.round(n) / 100;
57
+ }
58
+
59
+ function formatDollars(amount) {
60
+ const n = Number(amount);
61
+ if (!Number.isFinite(n)) return '$0.00';
62
+ return `$${n.toFixed(2)}`;
63
+ }
64
+
65
+ function normalizeBalance(data = {}) {
66
+ const raw = data && typeof data === 'object' ? data : {};
67
+ const credits = Number(raw.balance != null ? raw.balance : raw.credits);
68
+ const safeCredits = Number.isFinite(credits) ? credits : 0;
69
+ const usd = Number(raw.balance_usd);
70
+ const dollars = Number.isFinite(usd) ? usd : creditsToDollars(safeCredits);
71
+ return {
72
+ dollars,
73
+ credits: safeCredits,
74
+ balance_usd: dollars,
75
+ balance: safeCredits,
76
+ lifetime_purchased: raw.lifetime_purchased != null ? raw.lifetime_purchased : null,
77
+ lifetime_spent: raw.lifetime_spent != null ? raw.lifetime_spent : null,
78
+ };
79
+ }
80
+
81
+ function checkoutUrl(data = {}) {
82
+ const raw = data && typeof data === 'object' ? data : {};
83
+ return String(raw.checkout_url || raw.url || raw.checkoutUrl || '').trim();
84
+ }
85
+
86
+ function parseAmountUsd(args = [], fallback = 10) {
87
+ const values = [];
88
+ for (let i = 0; i < args.length; i += 1) {
89
+ const arg = String(args[i]);
90
+ if (arg === '--json' || arg === '--help' || arg === '-h' || arg === 'help') continue;
91
+ if (arg.startsWith('--')) continue;
92
+ values.push(arg);
93
+ }
94
+ if (values.length === 0) return { ok: true, amount: fallback };
95
+ const raw = String(values[0]).replace(/^\$/, '');
96
+ const amount = Number(raw);
97
+ if (!Number.isInteger(amount) || amount < 5 || amount > 500) {
98
+ return { ok: false, error: 'amount must be a whole dollar from 5 to 500' };
99
+ }
100
+ return { ok: true, amount };
101
+ }
102
+
103
+ module.exports = {
104
+ ROLLOUT_MESSAGE,
105
+ NOT_LOGGED_IN,
106
+ wantsJson,
107
+ wantsHelp,
108
+ tokenFrom,
109
+ isNotFound,
110
+ oneLine,
111
+ printResult,
112
+ formatDollars,
113
+ normalizeBalance,
114
+ checkoutUrl,
115
+ parseAmountUsd,
116
+ };
package/lib/engine-ask.js CHANGED
@@ -29,7 +29,7 @@ const MAX_ASK_PROMPT_BYTES = 16 * 1024;
29
29
  const MAX_ASK_TOTAL_PROMPT_BYTES = 64 * 1024;
30
30
  const MAX_ASK_OUTPUT_BYTES = 1024 * 1024;
31
31
  const ASK_STOP_GRACE_MS = 250;
32
- const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok']);
32
+ const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok', 'agy']);
33
33
  const READ_ONLY_PREAMBLE = [
34
34
  'This is a read-only request.',
35
35
  'Do not modify files, create worktrees, start background agents, or run commands with side effects.',
@@ -241,8 +241,16 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '') {
241
241
  args: ['--no-memory', '--no-subagents', '--permission-mode', 'plan', '--sandbox', 'read-only', ...(model ? ['--model', model] : []), '-p', request],
242
242
  };
243
243
  }
244
- if (engine === 'droid') {
245
- return { engine, bin: profile.bin, args: ['exec', '--disable-builtin-skills', request] };
244
+ if (engine === 'agy') {
245
+ // agy prompts for tool permission even in plan mode and a headless ask
246
+ // has no one to answer, so it denies itself `ls` and fails every ask
247
+ // (verified live 2026-08-19). The sandbox keeps the run read-only while
248
+ // skip-permissions lets sandboxed reads proceed unattended.
249
+ return {
250
+ engine,
251
+ bin: profile.bin,
252
+ args: ['--mode', 'plan', '--sandbox', '--dangerously-skip-permissions', ...(model ? ['--model', model] : []), '-p', request],
253
+ };
246
254
  }
247
255
  throw new Error(`engine ask has no read-only command for ${engine}`);
248
256
  }