atris 3.53.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.
- package/FOR_AGENTS.md +7 -0
- package/README.md +1 -1
- package/atris/policies/ANTISLOP.md +2 -1
- package/atris/skills/copy-editor/SKILL.md +2 -1
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +82 -33
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/bin/atris +21 -0
- package/bin/atris.js +563 -373
- package/commands/api-key.js +170 -0
- package/commands/auth.js +453 -43
- package/commands/autoland.js +3 -1
- package/commands/autopilot-front.js +58 -11
- package/commands/avail.js +5 -4
- package/commands/balance.js +55 -0
- package/commands/bench.js +29 -1
- package/commands/brain.js +63 -12
- package/commands/brainstorm.js +18 -0
- package/commands/business.js +149 -1
- package/commands/chat-scan.js +17 -4
- package/commands/close.js +5 -1
- package/commands/doctor.js +174 -0
- package/commands/dream.js +7 -0
- package/commands/drill.js +34 -2
- package/commands/engine.js +47 -35
- package/commands/errors.js +58 -8
- package/commands/experiments.js +14 -0
- package/commands/feedback.js +61 -2
- package/commands/fleet-report.js +27 -7
- package/commands/fleet.js +98 -11
- package/commands/founder.js +60 -21
- package/commands/human-missions.js +79 -4
- package/commands/improve.js +54 -4
- package/commands/init.js +68 -92
- package/commands/install.js +40 -0
- package/commands/integrations.js +399 -30
- package/commands/interview.js +8 -0
- package/commands/land.js +48 -17
- package/commands/learn.js +29 -4
- package/commands/log.js +51 -0
- package/commands/member.js +68 -10
- package/commands/mission.js +287 -50
- package/commands/now.js +15 -3
- package/commands/orb.js +97 -2
- package/commands/pack.js +83 -9
- package/commands/playbook.js +381 -0
- package/commands/plugin.js +16 -0
- package/commands/radar.js +31 -7
- package/commands/recap.js +128 -21
- package/commands/release.js +102 -2
- package/commands/review.js +5 -10
- package/commands/revisions.js +55 -0
- package/commands/run-front.js +12 -4
- package/commands/scout.js +12 -1
- package/commands/search.js +123 -28
- package/commands/sign.js +2 -2
- package/commands/signup.js +12 -3
- package/commands/site-deploy.js +17 -4
- package/commands/site.js +2 -2
- package/commands/skill-eval.js +289 -0
- package/commands/skill.js +56 -6
- package/commands/social.js +603 -0
- package/commands/spaceship.js +69 -6
- package/commands/stream.js +8 -1
- package/commands/study.js +32 -19
- package/commands/sync.js +28 -22
- package/commands/task.js +418 -60
- package/commands/teach.js +72 -63
- package/commands/terminal.js +25 -9
- package/commands/topup.js +88 -0
- package/commands/truth.js +52 -6
- package/commands/usage.js +91 -0
- package/commands/verify.js +3 -2
- package/commands/version.js +3 -1
- package/commands/voice.js +5 -1
- package/commands/who.js +69 -11
- package/commands/wiki.js +7 -4
- package/commands/wish.js +76 -5
- package/commands/workflow.js +327 -145
- package/commands/worktree.js +1 -0
- package/commands/write.js +5 -0
- package/commands/x-search.js +340 -0
- package/commands/xp.js +5 -1
- package/commands/youtube.js +551 -25
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +8 -1
- package/lib/auto-lessons.js +186 -0
- package/lib/autoland.js +4 -2
- package/lib/chat-log-scan.js +7 -4
- package/lib/cli-json.js +77 -0
- package/lib/cli-scope.js +26 -0
- package/lib/codex-flight.js +1 -0
- package/lib/conductor-artifacts.js +1 -1
- package/lib/context-gatherer.js +11 -0
- package/lib/developer-api.js +116 -0
- package/lib/feedback-triage.js +274 -0
- package/lib/first-minute.js +398 -0
- package/lib/fleet.js +115 -34
- package/lib/functional-owner.js +22 -0
- package/lib/known-commands.js +31 -5
- package/lib/member-scaffold.js +197 -0
- package/lib/mission-root.js +4 -2
- package/lib/noninteractive.js +88 -0
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +4 -0
- package/lib/task-proof.js +69 -8
- package/lib/verifier-quality.js +69 -0
- package/lib/wish-audit.js +2 -2
- package/lib/wish-delegate.js +10 -1
- package/lib/workspace-scaffold.js +270 -0
- package/package.json +3 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/utils/update-check.js +15 -0
|
@@ -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:
|
|
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
|
|
|
@@ -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))
|
|
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.' };
|
package/lib/chat-log-scan.js
CHANGED
|
@@ -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 (
|
|
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),
|
package/lib/cli-json.js
ADDED
|
@@ -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
|
+
};
|
package/lib/cli-scope.js
ADDED
|
@@ -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
|
+
};
|
package/lib/codex-flight.js
CHANGED
|
@@ -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 || ''));
|
package/lib/context-gatherer.js
CHANGED
|
@@ -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
|
+
};
|