atris 3.35.0 → 3.37.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/AGENTS.md +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const LEDGER_FILE = path.join('.atris', 'state', 'briefs.jsonl');
|
|
9
|
+
const OUTCOMES = new Set(['pass', 'fail', 'partial']);
|
|
10
|
+
const VERIFY_PATTERNS = Object.freeze([
|
|
11
|
+
/\bnode --test(?:\s+[^\n.;]*)?/i,
|
|
12
|
+
/\bnpm (?:test|run test)(?:\s+[^\n.;]*)?/i,
|
|
13
|
+
/\bpnpm (?:test|run test)(?:\s+[^\n.;]*)?/i,
|
|
14
|
+
/\byarn (?:test|run test)(?:\s+[^\n.;]*)?/i,
|
|
15
|
+
/\bbun test(?:\s+[^\n.;]*)?/i,
|
|
16
|
+
/\bpytest(?:\s+[^\n.;]*)?/i,
|
|
17
|
+
/\bgo test(?:\s+[^\n.;]*)?/i,
|
|
18
|
+
/\bcargo test(?:\s+[^\n.;]*)?/i,
|
|
19
|
+
/\bgit diff --check\b/i,
|
|
20
|
+
]);
|
|
21
|
+
const BOUNDED_SLICE_PATTERN = /\b(done criteria|done requires|exit criteria|acceptance criteria|check:|verify:|stop when|bounded|one bounded|smallest diff|only touch|do not touch|files changed)\b/i;
|
|
22
|
+
|
|
23
|
+
function ledgerPath(root = process.cwd()) {
|
|
24
|
+
return path.join(root, LEDGER_FILE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isoStamp(now = new Date()) {
|
|
28
|
+
return now instanceof Date ? now.toISOString() : new Date(now).toISOString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function briefId(now = new Date()) {
|
|
32
|
+
const compact = isoStamp(now).replace(/[-:.]/g, '').replace('T', '-').replace('Z', '');
|
|
33
|
+
return `brief-${compact}-${crypto.randomBytes(4).toString('hex')}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cleanString(value, fallback = '') {
|
|
37
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
38
|
+
return text || fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function appendJsonl(file, record) {
|
|
42
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
43
|
+
fs.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function currentGitValue(cwd, args) {
|
|
47
|
+
try {
|
|
48
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
49
|
+
return result.status === 0 ? String(result.stdout || '').trim() : '';
|
|
50
|
+
} catch {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function worktreeBaseRef(worktree, fallback = '') {
|
|
56
|
+
const wt = String(worktree || '').trim();
|
|
57
|
+
if (!wt || !fs.existsSync(wt)) return fallback || '';
|
|
58
|
+
try {
|
|
59
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(wt, '.atris', 'agent-worktree.json'), 'utf8'));
|
|
60
|
+
if (sidecar.base) return String(sidecar.base);
|
|
61
|
+
if (sidecar.checkout_base) return String(sidecar.checkout_base);
|
|
62
|
+
} catch {}
|
|
63
|
+
const branch = currentGitValue(wt, ['branch', '--show-current']);
|
|
64
|
+
if (branch) {
|
|
65
|
+
const configured = currentGitValue(wt, ['config', '--get', `branch.${branch}.atris-base`]);
|
|
66
|
+
if (configured) return configured;
|
|
67
|
+
}
|
|
68
|
+
return fallback || currentGitValue(wt, ['rev-parse', '--abbrev-ref', 'HEAD']) || '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function authorFromWorktree(worktree, fallback = 'orb') {
|
|
72
|
+
const wt = String(worktree || '').trim();
|
|
73
|
+
if (!wt || !fs.existsSync(wt)) return fallback;
|
|
74
|
+
try {
|
|
75
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(wt, '.atris', 'agent-worktree.json'), 'utf8'));
|
|
76
|
+
return cleanString(sidecar.owner || sidecar.member || sidecar.agent, fallback);
|
|
77
|
+
} catch {}
|
|
78
|
+
const branch = currentGitValue(wt, ['branch', '--show-current']);
|
|
79
|
+
if (branch) {
|
|
80
|
+
const configured = currentGitValue(wt, ['config', '--get', `branch.${branch}.atris-owner`]);
|
|
81
|
+
if (configured) return cleanString(configured, fallback);
|
|
82
|
+
}
|
|
83
|
+
return fallback;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeRecord(input = {}, { now = new Date() } = {}) {
|
|
87
|
+
const context = input.context && typeof input.context === 'object' ? input.context : {};
|
|
88
|
+
const worktree = cleanString(context.worktree || input.worktree || process.cwd(), process.cwd());
|
|
89
|
+
return {
|
|
90
|
+
brief_id: cleanString(input.brief_id, briefId(now)),
|
|
91
|
+
ts: cleanString(input.ts, isoStamp(now)),
|
|
92
|
+
author: cleanString(input.author, authorFromWorktree(worktree, 'orb')),
|
|
93
|
+
engine: cleanString(input.engine, 'unknown'),
|
|
94
|
+
...(input.task_id ? { task_id: cleanString(input.task_id) } : {}),
|
|
95
|
+
...(input.mission_id ? { mission_id: cleanString(input.mission_id) } : {}),
|
|
96
|
+
prompt_text: String(input.prompt_text || ''),
|
|
97
|
+
context: {
|
|
98
|
+
worktree,
|
|
99
|
+
base_ref: cleanString(context.base_ref || input.base_ref, worktreeBaseRef(worktree, '')),
|
|
100
|
+
},
|
|
101
|
+
outcome: input.outcome || null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function appendBriefRecord(root = process.cwd(), input = {}, options = {}) {
|
|
106
|
+
const record = normalizeRecord(input, options);
|
|
107
|
+
appendJsonl(ledgerPath(root), record);
|
|
108
|
+
return record;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mirrorBriefRecord(worktree, record) {
|
|
112
|
+
const wt = String(worktree || '').trim();
|
|
113
|
+
if (!wt || !fs.existsSync(wt)) return false;
|
|
114
|
+
const root = path.resolve(wt);
|
|
115
|
+
try {
|
|
116
|
+
appendJsonl(ledgerPath(root), record);
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readBriefLedger(root = process.cwd()) {
|
|
124
|
+
const file = ledgerPath(root);
|
|
125
|
+
if (!fs.existsSync(file)) return [];
|
|
126
|
+
return fs.readFileSync(file, 'utf8')
|
|
127
|
+
.split(/\r?\n/)
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
.map((line) => {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(line);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function latestBriefs(records = []) {
|
|
140
|
+
const byId = new Map();
|
|
141
|
+
for (const record of records) {
|
|
142
|
+
if (!record || !record.brief_id) continue;
|
|
143
|
+
byId.set(record.brief_id, record);
|
|
144
|
+
}
|
|
145
|
+
return [...byId.values()].sort((a, b) => Date.parse(b.outcome?.ts || b.ts || 0) - Date.parse(a.outcome?.ts || a.ts || 0));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeOutcome(outcome = {}, { now = new Date() } = {}) {
|
|
149
|
+
const result = cleanString(outcome.result).toLowerCase();
|
|
150
|
+
if (!OUTCOMES.has(result)) throw new Error(`brief outcome result must be one of ${[...OUTCOMES].join(', ')}`);
|
|
151
|
+
return {
|
|
152
|
+
result,
|
|
153
|
+
note: cleanString(outcome.note, ''),
|
|
154
|
+
ts: cleanString(outcome.ts, isoStamp(now)),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stampBriefOutcome(root = process.cwd(), id, outcome = {}, options = {}) {
|
|
159
|
+
const records = readBriefLedger(root);
|
|
160
|
+
const current = latestBriefs(records).find((record) => record.brief_id === id);
|
|
161
|
+
if (!current) return { ok: false, error: `brief not found: ${id}` };
|
|
162
|
+
const next = {
|
|
163
|
+
...current,
|
|
164
|
+
outcome: normalizeOutcome(outcome, options),
|
|
165
|
+
};
|
|
166
|
+
appendJsonl(ledgerPath(root), next);
|
|
167
|
+
return { ok: true, record: next };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function samePath(a, b) {
|
|
171
|
+
if (!a || !b) return false;
|
|
172
|
+
try {
|
|
173
|
+
return fs.realpathSync(a) === fs.realpathSync(b);
|
|
174
|
+
} catch {
|
|
175
|
+
return path.resolve(a) === path.resolve(b);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function stampLatestOpenBriefForWorktree(root = process.cwd(), outcome = {}, options = {}) {
|
|
180
|
+
const target = options.worktree || root;
|
|
181
|
+
const current = latestBriefs(readBriefLedger(root))
|
|
182
|
+
.find((record) => !record.outcome && samePath(record.context?.worktree, target));
|
|
183
|
+
if (!current) return { ok: false, error: 'no open brief for this worktree' };
|
|
184
|
+
return stampBriefOutcome(root, current.brief_id, outcome, options);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function findVerifyCommand(prompt) {
|
|
188
|
+
const text = String(prompt || '');
|
|
189
|
+
for (const pattern of VERIFY_PATTERNS) {
|
|
190
|
+
const match = text.match(pattern);
|
|
191
|
+
if (match) return match[0].trim();
|
|
192
|
+
}
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function wordCount(text) {
|
|
197
|
+
const words = String(text || '').trim().match(/\S+/g);
|
|
198
|
+
return words ? words.length : 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function elapsedMs(start, end) {
|
|
202
|
+
const a = Date.parse(start || '');
|
|
203
|
+
const b = Date.parse(end || '');
|
|
204
|
+
if (!Number.isFinite(a) || !Number.isFinite(b) || b < a) return null;
|
|
205
|
+
return b - a;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function briefSignals(record) {
|
|
209
|
+
const prompt = String(record?.prompt_text || '');
|
|
210
|
+
const verifyCommand = findVerifyCommand(prompt);
|
|
211
|
+
const hasBoundedSlice = BOUNDED_SLICE_PATTERN.test(prompt);
|
|
212
|
+
const outcomeTs = record?.outcome?.ts || '';
|
|
213
|
+
return {
|
|
214
|
+
prompt_chars: prompt.length,
|
|
215
|
+
prompt_words: wordCount(prompt),
|
|
216
|
+
verify_command: verifyCommand || null,
|
|
217
|
+
has_named_verify: Boolean(verifyCommand),
|
|
218
|
+
has_bounded_slice: hasBoundedSlice,
|
|
219
|
+
had_exit_criteria: Boolean(verifyCommand || hasBoundedSlice),
|
|
220
|
+
engine: cleanString(record?.engine, 'unknown'),
|
|
221
|
+
time_to_land_ms: record?.outcome?.result === 'pass' ? elapsedMs(record.ts, outcomeTs) : null,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function reviewItem(record) {
|
|
226
|
+
return {
|
|
227
|
+
...record,
|
|
228
|
+
outcome_group: record?.outcome?.result || 'open',
|
|
229
|
+
signals: briefSignals(record),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function groupReviewItems(items) {
|
|
234
|
+
const groups = { pass: [], partial: [], fail: [], open: [] };
|
|
235
|
+
for (const item of items) {
|
|
236
|
+
const key = groups[item.outcome_group] ? item.outcome_group : 'open';
|
|
237
|
+
groups[key].push(item);
|
|
238
|
+
}
|
|
239
|
+
return groups;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function landCount(items) {
|
|
243
|
+
return items.filter((item) => item.outcome?.result === 'pass').length;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function lessonLine(items, label) {
|
|
247
|
+
return `${label} land ${landCount(items)} of ${items.length}.`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function booleanLessons(items, field, withLabel, withoutLabel) {
|
|
251
|
+
const yes = items.filter((item) => item.signals[field]);
|
|
252
|
+
const no = items.filter((item) => !item.signals[field]);
|
|
253
|
+
const lines = [];
|
|
254
|
+
if (yes.length >= 5 && no.length >= 5) {
|
|
255
|
+
lines.push(`${withLabel} land ${landCount(yes)} of ${yes.length}; ${withoutLabel} land ${landCount(no)} of ${no.length}.`);
|
|
256
|
+
} else {
|
|
257
|
+
if (yes.length >= 5) lines.push(lessonLine(yes, withLabel));
|
|
258
|
+
if (no.length >= 5) lines.push(lessonLine(no, withoutLabel));
|
|
259
|
+
}
|
|
260
|
+
return lines;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildLessons(items) {
|
|
264
|
+
const lessons = [
|
|
265
|
+
...booleanLessons(items, 'has_named_verify', 'briefs with a named verify command', 'briefs without a named verify command'),
|
|
266
|
+
...booleanLessons(items, 'has_bounded_slice', 'briefs with bounded slice language', 'briefs without bounded slice language'),
|
|
267
|
+
...booleanLessons(items, 'had_exit_criteria', 'briefs with exit criteria', 'briefs without exit criteria'),
|
|
268
|
+
];
|
|
269
|
+
const byEngine = new Map();
|
|
270
|
+
for (const item of items) {
|
|
271
|
+
const engine = item.signals.engine || 'unknown';
|
|
272
|
+
byEngine.set(engine, [...(byEngine.get(engine) || []), item]);
|
|
273
|
+
}
|
|
274
|
+
for (const [engine, bucket] of byEngine.entries()) {
|
|
275
|
+
if (bucket.length >= 5) lessons.push(lessonLine(bucket, `${engine} briefs`));
|
|
276
|
+
}
|
|
277
|
+
return lessons;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function buildBriefReview(root = process.cwd(), { limit = 20, lessons = false } = {}) {
|
|
281
|
+
const all = latestBriefs(readBriefLedger(root)).map(reviewItem);
|
|
282
|
+
const recent = all.slice(0, Math.max(1, Number(limit) || 20));
|
|
283
|
+
const groups = groupReviewItems(recent);
|
|
284
|
+
return {
|
|
285
|
+
schema: 'atris.brief.review.v1',
|
|
286
|
+
generated_at: isoStamp(),
|
|
287
|
+
count: recent.length,
|
|
288
|
+
groups,
|
|
289
|
+
...(lessons ? { lessons: buildLessons(recent) } : {}),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function compactTitle(record) {
|
|
294
|
+
const prompt = String(record?.prompt_text || '').replace(/\s+/g, ' ').trim();
|
|
295
|
+
const fromTask = record?.task_id ? `task ${record.task_id}` : '';
|
|
296
|
+
const fromMission = record?.mission_id ? `mission ${record.mission_id}` : '';
|
|
297
|
+
const title = fromTask || fromMission || prompt.slice(0, 72) || 'untitled brief';
|
|
298
|
+
return title.length > 72 ? `${title.slice(0, 69)}...` : title;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function durationLabel(ms) {
|
|
302
|
+
if (ms === null || ms === undefined) return 'not landed';
|
|
303
|
+
const minutes = Math.round(ms / 60000);
|
|
304
|
+
if (minutes < 1) return 'under 1m';
|
|
305
|
+
if (minutes < 60) return `${minutes}m`;
|
|
306
|
+
const hours = Math.round(minutes / 60);
|
|
307
|
+
if (hours < 48) return `${hours}h`;
|
|
308
|
+
return `${Math.round(hours / 24)}d`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function renderBriefReview(review = {}, { lessons = false } = {}) {
|
|
312
|
+
const groups = review.groups || {};
|
|
313
|
+
const counts = ['pass', 'partial', 'fail', 'open'].map((key) => `${key} ${(groups[key] || []).length}`).join(', ');
|
|
314
|
+
const lines = [`brief review: ${review.count || 0} recent briefs | ${counts}`];
|
|
315
|
+
for (const key of ['pass', 'partial', 'fail', 'open']) {
|
|
316
|
+
const items = groups[key] || [];
|
|
317
|
+
lines.push('');
|
|
318
|
+
lines.push(`${key}: ${items.length ? 'recorded' : 'none'}`);
|
|
319
|
+
for (const item of items.slice(0, 5)) {
|
|
320
|
+
const signals = item.signals || {};
|
|
321
|
+
lines.push(`- ${signals.engine || item.engine || 'unknown'} | ${signals.prompt_words || 0} words | verify ${signals.has_named_verify ? 'yes' : 'no'} | exit ${signals.had_exit_criteria ? 'yes' : 'no'} | land ${durationLabel(signals.time_to_land_ms)} | ${compactTitle(item)}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (lessons) {
|
|
325
|
+
lines.push('');
|
|
326
|
+
const found = Array.isArray(review.lessons) ? review.lessons : [];
|
|
327
|
+
lines.push(found.length ? 'lessons:' : 'lessons: not enough data yet');
|
|
328
|
+
for (const lesson of found) lines.push(`- ${lesson}`);
|
|
329
|
+
}
|
|
330
|
+
return lines.join('\n');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = {
|
|
334
|
+
LEDGER_FILE,
|
|
335
|
+
OUTCOMES,
|
|
336
|
+
appendBriefRecord,
|
|
337
|
+
authorFromWorktree,
|
|
338
|
+
briefSignals,
|
|
339
|
+
buildBriefReview,
|
|
340
|
+
buildLessons,
|
|
341
|
+
ledgerPath,
|
|
342
|
+
latestBriefs,
|
|
343
|
+
mirrorBriefRecord,
|
|
344
|
+
normalizeRecord,
|
|
345
|
+
readBriefLedger,
|
|
346
|
+
renderBriefReview,
|
|
347
|
+
stampBriefOutcome,
|
|
348
|
+
stampLatestOpenBriefForWorktree,
|
|
349
|
+
worktreeBaseRef,
|
|
350
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { apiRequestJson } = require('../utils/api');
|
|
6
|
+
const { loadCredentials } = require('../utils/auth');
|
|
7
|
+
|
|
8
|
+
const VALID_CLOUD_LANES = new Set(['fast', 'pro', 'max']);
|
|
9
|
+
const TERMINAL_CLOUD_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
10
|
+
const DEFAULT_POLL_INTERVAL_MS = 10_000;
|
|
11
|
+
const DEFAULT_MAX_WATCH_MS = 30 * 60 * 1_000;
|
|
12
|
+
|
|
13
|
+
class CloudMissionError extends Error {
|
|
14
|
+
constructor(message, exitCode = 1) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'CloudMissionError';
|
|
17
|
+
this.exitCode = exitCode;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function valueFlag(args, name) {
|
|
22
|
+
const prefix = `${name}=`;
|
|
23
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
24
|
+
const arg = String(args[i]);
|
|
25
|
+
if (arg.startsWith(prefix)) return { present: true, value: arg.slice(prefix.length), index: i };
|
|
26
|
+
if (arg === name) {
|
|
27
|
+
const next = args[i + 1];
|
|
28
|
+
return {
|
|
29
|
+
present: true,
|
|
30
|
+
value: next && !String(next).startsWith('--') ? String(next) : '',
|
|
31
|
+
index: i,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { present: false, value: '', index: -1 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseCloudRunArgs(args) {
|
|
39
|
+
const laneFlag = valueFlag(args, '--lane');
|
|
40
|
+
const agentFlag = valueFlag(args, '--agent');
|
|
41
|
+
if (laneFlag.present && !laneFlag.value) {
|
|
42
|
+
throw new CloudMissionError('--lane requires one of: fast, pro, max', 2);
|
|
43
|
+
}
|
|
44
|
+
if (agentFlag.present && !agentFlag.value) {
|
|
45
|
+
throw new CloudMissionError('--agent requires an agent id', 2);
|
|
46
|
+
}
|
|
47
|
+
const lane = laneFlag.value || 'fast';
|
|
48
|
+
if (!VALID_CLOUD_LANES.has(lane)) {
|
|
49
|
+
throw new CloudMissionError(`invalid --lane "${lane}". expected fast, pro, or max`, 2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const textParts = [];
|
|
53
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
54
|
+
const arg = String(args[i]);
|
|
55
|
+
if (arg === '--cloud' || arg === '--json') continue;
|
|
56
|
+
if (arg === '--lane') {
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg.startsWith('--lane=')) continue;
|
|
61
|
+
if (arg === '--agent') {
|
|
62
|
+
i += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg.startsWith('--agent=')) continue;
|
|
66
|
+
if (arg.startsWith('--')) {
|
|
67
|
+
throw new CloudMissionError(`unsupported cloud mission flag: ${arg}`, 2);
|
|
68
|
+
}
|
|
69
|
+
textParts.push(arg);
|
|
70
|
+
}
|
|
71
|
+
const text = textParts.join(' ').trim();
|
|
72
|
+
if (!text) {
|
|
73
|
+
throw new CloudMissionError('usage: atris mission run "<intent>" --cloud [--lane fast|pro|max]', 2);
|
|
74
|
+
}
|
|
75
|
+
const parsed = { text, lane, asJson: args.includes('--json') };
|
|
76
|
+
if (agentFlag.present) parsed.agentId = agentFlag.value;
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseCloudStatusArgs(args) {
|
|
81
|
+
const cloudFlag = valueFlag(args, '--cloud');
|
|
82
|
+
if (!cloudFlag.present || !cloudFlag.value) {
|
|
83
|
+
throw new CloudMissionError('usage: atris mission status --cloud <task_id> [--watch]', 2);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
taskId: cloudFlag.value,
|
|
87
|
+
watch: args.includes('--watch'),
|
|
88
|
+
asJson: args.includes('--json'),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function credentialsToken(load = loadCredentials) {
|
|
93
|
+
const credentials = load();
|
|
94
|
+
const token = credentials && String(credentials.token || '').trim();
|
|
95
|
+
if (!token) throw new CloudMissionError('not logged in. run: atris login', 1);
|
|
96
|
+
return token;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requestError(result) {
|
|
100
|
+
const status = result && result.status ? ` (${result.status})` : '';
|
|
101
|
+
const backendDetail = result && result.data && result.data.detail;
|
|
102
|
+
const errorDetail = result && (result.error || backendDetail);
|
|
103
|
+
const detail = errorDetail ? `: ${errorDetail}` : '';
|
|
104
|
+
const hint = /business/i.test(String(backendDetail || errorDetail || ''))
|
|
105
|
+
? '\ntry: --agent <id of a business-attached agent>'
|
|
106
|
+
: '';
|
|
107
|
+
return new CloudMissionError(`cloud mission request failed${status}${detail}${hint}`, 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Every cloud mission carries the receipt voice contract: the wish text is the
|
|
111
|
+
// operator's words, but how the cloud reports back is ours. Without this, the
|
|
112
|
+
// cloud invents its own receipt style (emoji, em dashes, made-up timestamps).
|
|
113
|
+
const RECEIPT_VOICE_CONTRACT = 'When you finish, report back in plain sentences: say what you did and how you know it worked. No emoji, no em dashes, no headings, and never invent timestamps or ids; only state times and identifiers you were actually given.';
|
|
114
|
+
|
|
115
|
+
function withReceiptVoice(text) {
|
|
116
|
+
const body = String(text || '').trim();
|
|
117
|
+
if (!body || body.includes(RECEIPT_VOICE_CONTRACT)) return body;
|
|
118
|
+
return `${body}\n\n${RECEIPT_VOICE_CONTRACT}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function enqueueCloudMission({ text, lane = 'fast', agentId }, deps = {}) {
|
|
122
|
+
if (!VALID_CLOUD_LANES.has(lane)) {
|
|
123
|
+
throw new CloudMissionError(`invalid --lane "${lane}". expected fast, pro, or max`, 2);
|
|
124
|
+
}
|
|
125
|
+
const token = credentialsToken(deps.loadCredentials || loadCredentials);
|
|
126
|
+
const request = deps.apiRequestJson || apiRequestJson;
|
|
127
|
+
const body = { text: withReceiptVoice(text), lane };
|
|
128
|
+
if (agentId) body.agent_id = agentId;
|
|
129
|
+
const response = await request('/atris2/missions', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
token,
|
|
132
|
+
body,
|
|
133
|
+
});
|
|
134
|
+
if (!response || !response.ok) throw requestError(response);
|
|
135
|
+
if (!response.data || !response.data.task_id) {
|
|
136
|
+
throw new CloudMissionError('cloud mission response did not include task_id', 1);
|
|
137
|
+
}
|
|
138
|
+
return response.data;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function fetchCloudMissionStatus(taskId, deps = {}) {
|
|
142
|
+
const token = credentialsToken(deps.loadCredentials || loadCredentials);
|
|
143
|
+
const request = deps.apiRequestJson || apiRequestJson;
|
|
144
|
+
const response = await request(`/atris2/missions/${encodeURIComponent(taskId)}`, {
|
|
145
|
+
method: 'GET',
|
|
146
|
+
token,
|
|
147
|
+
});
|
|
148
|
+
if (!response || !response.ok) throw requestError(response);
|
|
149
|
+
if (!response.data || !response.data.task_id || !response.data.status) {
|
|
150
|
+
throw new CloudMissionError('cloud mission response did not include task_id and status', 1);
|
|
151
|
+
}
|
|
152
|
+
return response.data;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function appendCloudMissionReceipt(root, receipt) {
|
|
156
|
+
const stateDir = path.join(root, '.atris', 'state');
|
|
157
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
158
|
+
const file = path.join(stateDir, 'missions.jsonl');
|
|
159
|
+
fs.appendFileSync(file, `${JSON.stringify(receipt)}\n`, 'utf8');
|
|
160
|
+
return file;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function cloudMissionSummary(mission) {
|
|
164
|
+
if (mission && mission.result && typeof mission.result === 'object' && mission.result.summary) {
|
|
165
|
+
return String(mission.result.summary);
|
|
166
|
+
}
|
|
167
|
+
if (mission && typeof mission.result === 'string' && mission.result.trim()) return mission.result.trim();
|
|
168
|
+
if (mission && mission.summary) return String(mission.summary);
|
|
169
|
+
return 'not available yet';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function cloudStatusLines(mission) {
|
|
173
|
+
return [
|
|
174
|
+
`cloud mission: ${mission.task_id}`,
|
|
175
|
+
` status: ${mission.status}`,
|
|
176
|
+
` lane: ${mission.lane || 'unknown'}`,
|
|
177
|
+
` summary: ${cloudMissionSummary(mission)}`,
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function printCloudStatus(mission, asJson, log) {
|
|
182
|
+
if (asJson) {
|
|
183
|
+
log(JSON.stringify(mission, null, 2));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (const line of cloudStatusLines(mission)) log(line);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function runCloudMissionCommand(args, options = {}) {
|
|
190
|
+
const log = options.log || console.log;
|
|
191
|
+
const error = options.error || console.error;
|
|
192
|
+
try {
|
|
193
|
+
const parsed = parseCloudRunArgs(args);
|
|
194
|
+
const mission = await enqueueCloudMission(parsed, options);
|
|
195
|
+
const receipt = {
|
|
196
|
+
cloud: true,
|
|
197
|
+
task_id: mission.task_id,
|
|
198
|
+
lane: mission.lane || parsed.lane,
|
|
199
|
+
text: parsed.text,
|
|
200
|
+
};
|
|
201
|
+
appendCloudMissionReceipt(options.root || process.cwd(), receipt);
|
|
202
|
+
if (parsed.asJson) {
|
|
203
|
+
log(JSON.stringify({ ok: true, action: 'cloud_mission_enqueued', mission, receipt }, null, 2));
|
|
204
|
+
} else {
|
|
205
|
+
log(`cloud mission queued: ${mission.task_id}`);
|
|
206
|
+
log(`lane: ${receipt.lane}`);
|
|
207
|
+
log(`next: atris mission status --cloud ${mission.task_id}`);
|
|
208
|
+
}
|
|
209
|
+
return { exitCode: 0, mission, receipt };
|
|
210
|
+
} catch (caught) {
|
|
211
|
+
error(caught.message || String(caught));
|
|
212
|
+
return { exitCode: caught.exitCode || 1, error: caught };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function statusCloudMissionCommand(args, options = {}) {
|
|
217
|
+
const log = options.log || console.log;
|
|
218
|
+
const error = options.error || console.error;
|
|
219
|
+
const sleep = options.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
220
|
+
const now = options.now || Date.now;
|
|
221
|
+
const pollIntervalMs = options.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS;
|
|
222
|
+
const maxWatchMs = options.maxWatchMs || DEFAULT_MAX_WATCH_MS;
|
|
223
|
+
try {
|
|
224
|
+
const parsed = parseCloudStatusArgs(args);
|
|
225
|
+
const startedAt = now();
|
|
226
|
+
while (true) {
|
|
227
|
+
const mission = await fetchCloudMissionStatus(parsed.taskId, options);
|
|
228
|
+
printCloudStatus(mission, parsed.asJson, log);
|
|
229
|
+
const terminal = TERMINAL_CLOUD_STATUSES.has(mission.status);
|
|
230
|
+
if (!parsed.watch) return { exitCode: 0, mission };
|
|
231
|
+
if (terminal) return { exitCode: mission.status === 'completed' ? 0 : 1, mission };
|
|
232
|
+
const elapsed = now() - startedAt;
|
|
233
|
+
if (elapsed >= maxWatchMs) {
|
|
234
|
+
error(`cloud mission watch timed out after ${Math.round(maxWatchMs / 60_000)} minutes`);
|
|
235
|
+
return { exitCode: 1, mission, timedOut: true };
|
|
236
|
+
}
|
|
237
|
+
await sleep(Math.min(pollIntervalMs, maxWatchMs - elapsed));
|
|
238
|
+
}
|
|
239
|
+
} catch (caught) {
|
|
240
|
+
error(caught.message || String(caught));
|
|
241
|
+
return { exitCode: caught.exitCode || 1, error: caught };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
VALID_CLOUD_LANES,
|
|
247
|
+
TERMINAL_CLOUD_STATUSES,
|
|
248
|
+
RECEIPT_VOICE_CONTRACT,
|
|
249
|
+
withReceiptVoice,
|
|
250
|
+
parseCloudRunArgs,
|
|
251
|
+
parseCloudStatusArgs,
|
|
252
|
+
enqueueCloudMission,
|
|
253
|
+
fetchCloudMissionStatus,
|
|
254
|
+
appendCloudMissionReceipt,
|
|
255
|
+
cloudMissionSummary,
|
|
256
|
+
cloudStatusLines,
|
|
257
|
+
runCloudMissionCommand,
|
|
258
|
+
statusCloudMissionCommand,
|
|
259
|
+
};
|