atris 3.35.0 → 3.36.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 +565 -265
- 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 +3029 -339
- 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 +79 -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,776 @@
|
|
|
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
|
+
const { freezeMissionVerifier, listMissions, markMissionReviewReady } = require('./mission');
|
|
8
|
+
const { readWishes } = require('../lib/wish-store');
|
|
9
|
+
const {
|
|
10
|
+
engineRegistryView,
|
|
11
|
+
resolveEngineForRole,
|
|
12
|
+
resolveRegisteredEngine,
|
|
13
|
+
} = require('../lib/engine-registry');
|
|
14
|
+
const { parseVerifyCommand } = require('../lib/auto-accept-certified');
|
|
15
|
+
const fleet = require('../lib/fleet');
|
|
16
|
+
|
|
17
|
+
const TERMINAL_MISSION_STATUSES = new Set(['complete', 'stopped']);
|
|
18
|
+
const WAITING_WISH_STATUSES = new Set(['needs_input', 'waiting_input', 'waiting_on_operator']);
|
|
19
|
+
const OUTBOUND_ACTION = /^(?:(?:please|kindly|execute|go|run)\s+)*(?:(?:git\s+)?push(?:ing)?|deploy(?:ing|ment)?|(?:a\s+)?(?:production|prod)\s+deploy(?:ing|ment)?|(?:npm\s+)?publish(?:ing)?|send(?:ing)?|email(?:ing)?|notify(?:ing)?|messag(?:e|ing)|charge|refund|purchase|rotate|revoke|drop|truncate|upload|share|transfer|pay|book|schedule|invite|delete|release|land|ship|curl|wget|ssh)\b|^merg(?:e|ing)\b(?!\s+sort\b)|^post\b.{0,120}\b(?:to|on)\s+(?:x|twitter|linkedin|facebook|slack|discord|the\s+customer|a\s+customer|customers?|external)\b/i;
|
|
20
|
+
const COMPOUND_ACTION_BOUNDARY = /\s*(?:[;,:.!?&+\/]|\b(?:and(?:\s+then)?|then|also|plus|before|after|afterwards|while|meanwhile|followed(?:\s+|-)by)\b)\s*/i;
|
|
21
|
+
const OUTBOUND_TARGET_ACTION = /\b(?:push|merge|deploy|publish|release|land|ship)\b.{0,120}\b(?:to|into|on)\s+(?:origin\/)?(?:production|prod|master|main)\b|\b(?:post|send|email|upload|share|transfer)\b.{0,120}\b(?:to|with)\s+(?:x|twitter|linkedin|facebook|slack|discord|customers?|external\b)/i;
|
|
22
|
+
const LOCAL_INTENT = /^(add|analyze|audit|build|change|check|clean|create|debug|diagnose|document|edit|ensure|find|fix|harden|implement|improve|inspect|investigate|locate|make|merge\s+sort|optimize|order\s+validation|patch|post\s+(?:\/|api\b|endpoint\b|handler\b|route\b|request\b)|reduce|refactor|remove|rename|research|review|run\s+(the\s+)?(tests?|checks?|lint|build)|simplify|test|trace|update|validate|work\s+on|write)\b/i;
|
|
23
|
+
const ONE_LAP_LOCK_STALE_MS = 6 * 60 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
function ownCli(root, args, cwd = root) {
|
|
26
|
+
const bin = path.resolve(__dirname, '..', 'bin', 'atris.js');
|
|
27
|
+
const result = spawnSync(process.execPath, [bin, ...args], {
|
|
28
|
+
cwd,
|
|
29
|
+
env: process.env,
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
timeout: 20 * 60 * 1000,
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
status: result.status,
|
|
35
|
+
signal: result.signal,
|
|
36
|
+
stdout: String(result.stdout || ''),
|
|
37
|
+
stderr: String(result.stderr || ''),
|
|
38
|
+
error: result.error || null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseJsonOutput(result) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(String(result && result.stdout || '').trim());
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizedAsk(value) {
|
|
51
|
+
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function shellQuote(value) {
|
|
55
|
+
const text = String(value || '');
|
|
56
|
+
if (/^[A-Za-z0-9_./:@+-]+$/.test(text)) return text;
|
|
57
|
+
return `'${text.replace(/'/g, `'"'"'`)}'`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function oneLapRetryCommand(ask, options = {}) {
|
|
61
|
+
const parts = ['atris', shellQuote(ask)];
|
|
62
|
+
if (options.engine) parts.push('--engine', shellQuote(options.engine));
|
|
63
|
+
if (options.verifier) parts.push('--verify', shellQuote(options.verifier));
|
|
64
|
+
parts.push('--json');
|
|
65
|
+
return parts.join(' ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function wishAnswerCommand(wish) {
|
|
69
|
+
const ref = wish && (wish.id || (wish.n ? `#${wish.n}` : ''));
|
|
70
|
+
return ref
|
|
71
|
+
? `atris wish answer ${shellQuote(ref)} ${shellQuote('<answer>')}`
|
|
72
|
+
: `atris wish answer ${shellQuote('<answer>')}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function askLockFile(root, ask) {
|
|
76
|
+
const key = crypto.createHash('sha256').update(normalizedAsk(ask)).digest('hex').slice(0, 32);
|
|
77
|
+
return path.join(root, '.atris', 'state', 'one-lap-locks', `${key}.lock`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function processIsAlive(pid) {
|
|
81
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
82
|
+
try {
|
|
83
|
+
process.kill(pid, 0);
|
|
84
|
+
return true;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return error && error.code === 'EPERM';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readAskLock(file) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function acquireAskLock(root, ask) {
|
|
99
|
+
const file = askLockFile(root, ask);
|
|
100
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
101
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
102
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
103
|
+
try {
|
|
104
|
+
const fd = fs.openSync(file, 'wx', 0o600);
|
|
105
|
+
try {
|
|
106
|
+
fs.writeFileSync(fd, `${JSON.stringify({ pid: process.pid, token, ask: normalizedAsk(ask), started_at: new Date().toISOString() })}\n`);
|
|
107
|
+
} finally {
|
|
108
|
+
fs.closeSync(fd);
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, file, token };
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
113
|
+
const holder = readAskLock(file);
|
|
114
|
+
let ageMs = 0;
|
|
115
|
+
try { ageMs = Date.now() - fs.statSync(file).mtimeMs; } catch {}
|
|
116
|
+
if ((holder && processIsAlive(Number(holder.pid))) || (!holder && ageMs < ONE_LAP_LOCK_STALE_MS)) {
|
|
117
|
+
return { ok: false, file, holder };
|
|
118
|
+
}
|
|
119
|
+
try { fs.unlinkSync(file); } catch (unlinkError) {
|
|
120
|
+
if (!unlinkError || unlinkError.code !== 'ENOENT') return { ok: false, file, holder };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { ok: false, file, holder: readAskLock(file) };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function releaseAskLock(lock) {
|
|
128
|
+
if (!lock || !lock.ok) return;
|
|
129
|
+
const current = readAskLock(lock.file);
|
|
130
|
+
if (!current || current.token !== lock.token) return;
|
|
131
|
+
try { fs.unlinkSync(lock.file); } catch (error) {
|
|
132
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function newestFirst(rows) {
|
|
137
|
+
return [...rows].sort((a, b) => String(b.updated_at || b.ts || b.first_ts || '')
|
|
138
|
+
.localeCompare(String(a.updated_at || a.ts || a.first_ts || '')));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function activeWishForAsk(root, ask) {
|
|
142
|
+
const missions = new Map(listMissions(root).map((mission) => [mission.id, mission]));
|
|
143
|
+
const matches = newestFirst(readWishes(root).filter((wish) => normalizedAsk(wish.text) === normalizedAsk(ask)));
|
|
144
|
+
for (const wish of matches) {
|
|
145
|
+
if (WAITING_WISH_STATUSES.has(String(wish.status || ''))) return { wish, mission: null };
|
|
146
|
+
const mission = missions.get(wish.mission_id);
|
|
147
|
+
if (mission && !TERMINAL_MISSION_STATUSES.has(String(mission.status || ''))) return { wish, mission };
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readyExecutor(root, preferred = '') {
|
|
153
|
+
if (preferred) {
|
|
154
|
+
const selected = resolveRegisteredEngine(preferred, root);
|
|
155
|
+
if (!selected.roles.includes('executor')) throw new Error(`engine ${selected.id} is not an executor`);
|
|
156
|
+
if (!selected.health || selected.health.status !== 'ready') throw new Error(`engine ${selected.id} is not ready`);
|
|
157
|
+
if (!fleet.FLEET_CAPABLE.includes(selected.id)) throw new Error(`engine ${selected.id} cannot build headlessly`);
|
|
158
|
+
return selected;
|
|
159
|
+
}
|
|
160
|
+
const routed = resolveEngineForRole('executor', root);
|
|
161
|
+
if (routed && fleet.FLEET_CAPABLE.includes(routed.id)) return routed;
|
|
162
|
+
return engineRegistryView(root)
|
|
163
|
+
.filter((engine) => engine.roles.includes('executor'))
|
|
164
|
+
.filter((engine) => engine.health && engine.health.status === 'ready')
|
|
165
|
+
.filter((engine) => fleet.FLEET_CAPABLE.includes(engine.id))
|
|
166
|
+
.sort((a, b) => Number(a.fallback_order) - Number(b.fallback_order))[0] || null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function readyValidators(root, preferred = '', exclude = '') {
|
|
170
|
+
const blocked = String(exclude || '').trim();
|
|
171
|
+
return engineRegistryView(root)
|
|
172
|
+
.filter((engine) => engine.roles.includes('validator'))
|
|
173
|
+
.filter((engine) => engine.health && engine.health.status === 'ready')
|
|
174
|
+
.filter((engine) => engine.id !== blocked)
|
|
175
|
+
.sort((a, b) => {
|
|
176
|
+
const aPreferred = preferred && a.id === preferred ? 0 : 1;
|
|
177
|
+
const bPreferred = preferred && b.id === preferred ? 0 : 1;
|
|
178
|
+
return aPreferred - bPreferred
|
|
179
|
+
|| Number(a.fallback_order) - Number(b.fallback_order)
|
|
180
|
+
|| String(a.id).localeCompare(String(b.id));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function taskById(runCli, taskId) {
|
|
185
|
+
const result = runCli(['task', 'show', taskId, '--json']);
|
|
186
|
+
if (!result || result.status !== 0) return null;
|
|
187
|
+
return parseJsonOutput(result);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function missionVerifier(mission) {
|
|
191
|
+
return String(mission && (mission.effective_verifier || mission.verifier) || '').trim();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function requestAction(title) {
|
|
195
|
+
return String(title || '')
|
|
196
|
+
.replace(/\s+/g, ' ')
|
|
197
|
+
.trim()
|
|
198
|
+
.replace(/^(?:(?:please|kindly)\s+|(?:can|could|would|will)\s+you\s+(?:please\s+)?|i\s+(?:need|want)\s+you\s+to\s+|help\s+me(?:\s+to)?\s+)+/i, '')
|
|
199
|
+
.trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function includesOutboundAction(title) {
|
|
203
|
+
const text = String(title || '').replace(/\s+/g, ' ').trim();
|
|
204
|
+
if (OUTBOUND_TARGET_ACTION.test(text)) return true;
|
|
205
|
+
return text.split(COMPOUND_ACTION_BOUNDARY)
|
|
206
|
+
.map(requestAction)
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
.some((clause) => OUTBOUND_ACTION.test(clause));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function oneLapSafetyIssue(task) {
|
|
212
|
+
if (!fleet.isSafeLane(task)) return 'the task is in a protected lane';
|
|
213
|
+
const title = String(task && task.title || '').replace(/\s+/g, ' ').trim();
|
|
214
|
+
const action = requestAction(title);
|
|
215
|
+
if (includesOutboundAction(title)) {
|
|
216
|
+
return 'the request includes an outbound or irreversible action';
|
|
217
|
+
}
|
|
218
|
+
if (!LOCAL_INTENT.test(action)) return 'one lap only dispatches local build, test, review, or research work';
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function directoryHasTests(root, dirName) {
|
|
223
|
+
const start = path.join(root, dirName);
|
|
224
|
+
if (!fs.existsSync(start)) return false;
|
|
225
|
+
const pending = [{ dir: start, depth: 0 }];
|
|
226
|
+
while (pending.length) {
|
|
227
|
+
const { dir, depth } = pending.shift();
|
|
228
|
+
let entries = [];
|
|
229
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (entry.isFile() && /(?:^|[._-])(test|spec)\.(?:c|m)?js$/i.test(entry.name)) return true;
|
|
232
|
+
if (entry.isDirectory() && depth < 2 && !entry.name.startsWith('.')) {
|
|
233
|
+
pending.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function projectVerifier(root) {
|
|
241
|
+
try {
|
|
242
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
243
|
+
const scripts = pkg && pkg.scripts || {};
|
|
244
|
+
const testScript = String(scripts.test || '').trim();
|
|
245
|
+
if (testScript && !/no test specified|exit 1/i.test(testScript)) return 'npm test';
|
|
246
|
+
for (const name of ['test:fast', 'test:unit', 'check']) {
|
|
247
|
+
if (String(scripts[name] || '').trim()) return `npm run ${name}`;
|
|
248
|
+
}
|
|
249
|
+
} catch {}
|
|
250
|
+
if (directoryHasTests(root, 'test') || directoryHasTests(root, 'tests')) return 'node --test';
|
|
251
|
+
if (fs.existsSync(path.join(root, 'tsconfig.json'))) return 'tsc';
|
|
252
|
+
if (fs.existsSync(path.join(root, 'pyproject.toml')) || fs.existsSync(path.join(root, 'pytest.ini'))) {
|
|
253
|
+
return 'python -m pytest';
|
|
254
|
+
}
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function relativeReceipt(root, receipt) {
|
|
259
|
+
return receipt ? path.relative(root, receipt) : '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function passingReadyProof(candidate, { taskId, wishId, missionId, verifier }) {
|
|
263
|
+
if (!candidate || !candidate.receipt || !candidate.ready) return false;
|
|
264
|
+
const { receipt, ready } = candidate;
|
|
265
|
+
const context = receipt.context || {};
|
|
266
|
+
const readyVerifier = ready.verifier_result || {};
|
|
267
|
+
const resultVerifier = receipt.result && receipt.result.verifier_result || {};
|
|
268
|
+
const validator = receipt.result && receipt.result.validator_result || {};
|
|
269
|
+
return receipt.schema === 'atris.dispatch_receipt.v1'
|
|
270
|
+
&& receipt.review_only === true
|
|
271
|
+
&& context.source === 'one_lap'
|
|
272
|
+
&& Array.isArray(receipt.tasks)
|
|
273
|
+
&& receipt.tasks.includes(taskId)
|
|
274
|
+
&& receipt.result
|
|
275
|
+
&& receipt.result.passed === true
|
|
276
|
+
&& receipt.result.master_boundary_enforced === true
|
|
277
|
+
&& receipt.result.master_unchanged === true
|
|
278
|
+
&& ready.task === taskId
|
|
279
|
+
&& ready.review_recorded === true
|
|
280
|
+
&& readyVerifier.passed === true
|
|
281
|
+
&& readyVerifier.status === 0
|
|
282
|
+
&& readyVerifier.command === verifier
|
|
283
|
+
&& resultVerifier.passed === true
|
|
284
|
+
&& resultVerifier.status === 0
|
|
285
|
+
&& resultVerifier.command === verifier
|
|
286
|
+
&& validator.passed === true
|
|
287
|
+
&& validator.independent === true
|
|
288
|
+
&& validator.worktree_unchanged === true
|
|
289
|
+
&& Boolean(validator.engine)
|
|
290
|
+
&& Boolean(validator.executor_engine)
|
|
291
|
+
&& validator.engine !== validator.executor_engine
|
|
292
|
+
&& validator.executor_engine === ready.engine
|
|
293
|
+
&& (!wishId || context.wish_id === wishId)
|
|
294
|
+
&& (!missionId || context.mission_id === missionId);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function findDispatchReceipt(root, taskId, proof = null) {
|
|
298
|
+
const dir = path.join(root, 'atris', 'runs');
|
|
299
|
+
let files = [];
|
|
300
|
+
try {
|
|
301
|
+
files = fs.readdirSync(dir).filter((name) => /^dispatch-.+\.json$/.test(name)).sort().reverse();
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
for (const name of files) {
|
|
306
|
+
try {
|
|
307
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
308
|
+
if (Array.isArray(receipt.tasks) && receipt.tasks.includes(taskId)) {
|
|
309
|
+
const candidate = {
|
|
310
|
+
path: path.join('atris', 'runs', name),
|
|
311
|
+
receipt,
|
|
312
|
+
ready: Array.isArray(receipt.ready) ? receipt.ready.find((row) => row.task === taskId) || null : null,
|
|
313
|
+
};
|
|
314
|
+
if (!proof || passingReadyProof(candidate, { ...proof, taskId })) return candidate;
|
|
315
|
+
}
|
|
316
|
+
} catch {}
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function resultBase({ status, ask, wish, mission, task, engine, validator, verifier }) {
|
|
322
|
+
return {
|
|
323
|
+
schema: 'atris.one_lap.v1',
|
|
324
|
+
ok: status === 'done' || status === 'waiting_input',
|
|
325
|
+
status,
|
|
326
|
+
ask,
|
|
327
|
+
wish_id: wish && wish.id || null,
|
|
328
|
+
task_id: task && task.display_id || wish && wish.task_id || null,
|
|
329
|
+
mission_id: mission && mission.id || wish && wish.mission_id || null,
|
|
330
|
+
engine: engine && engine.id || wish && wish.engine || null,
|
|
331
|
+
validator: validator && validator.id || wish && wish.validator || null,
|
|
332
|
+
verifier: verifier || null,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function renderText(result) {
|
|
337
|
+
const status = result.status === 'waiting_input'
|
|
338
|
+
? 'waiting on you'
|
|
339
|
+
: String(result.status || 'stuck').replace(/_/g, ' ');
|
|
340
|
+
console.log(`lap: ${status}`);
|
|
341
|
+
if (result.question) console.log(`question: ${result.question}`);
|
|
342
|
+
if (result.changed) console.log(`changed: ${result.changed}`);
|
|
343
|
+
if (result.reason) console.log(`why it matters: ${result.reason}`);
|
|
344
|
+
if (result.checked) console.log(`how i checked: ${result.checked}`);
|
|
345
|
+
if (result.tested) console.log(`what i tested: ${result.tested}`);
|
|
346
|
+
if (result.receipt) console.log(`proof: ${result.receipt}`);
|
|
347
|
+
if (result.next_action) console.log(`next: ${result.next_action}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function emit(result, asJson) {
|
|
351
|
+
if (asJson) console.log(JSON.stringify(result, null, 2));
|
|
352
|
+
else renderText(result);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function waitingResult({ ask, wish, mission = null, task = null, question = '', nextAction = '' }) {
|
|
356
|
+
return {
|
|
357
|
+
...resultBase({ status: 'waiting_input', ask, wish, mission, task }),
|
|
358
|
+
question: question || 'the existing lap needs operator input before it can continue',
|
|
359
|
+
next_action: nextAction || (wish && wish.id ? `atris wish show ${shellQuote(wish.id)}` : 'atris wish list'),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function runOneLap(ask, options = {}) {
|
|
364
|
+
const root = path.resolve(options.root || process.cwd());
|
|
365
|
+
const asJson = options.asJson === true;
|
|
366
|
+
const runCli = options.ownCli || ((args, cwd = root) => ownCli(root, args, cwd));
|
|
367
|
+
const progress = options.progress || ((line) => process.stderr.write(`${line}\n`));
|
|
368
|
+
const text = String(ask || '').replace(/\s+/g, ' ').trim();
|
|
369
|
+
if (!text) {
|
|
370
|
+
const result = { schema: 'atris.one_lap.v1', ok: false, status: 'stuck', reason: 'a request is required', next_action: 'atris "<request>"' };
|
|
371
|
+
emit(result, asJson);
|
|
372
|
+
return 2;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const lapLock = acquireAskLock(root, text);
|
|
376
|
+
if (!lapLock.ok) {
|
|
377
|
+
const running = activeWishForAsk(root, text);
|
|
378
|
+
const runningWish = running && running.wish || null;
|
|
379
|
+
const runningMission = running && running.mission || null;
|
|
380
|
+
const runningTask = runningWish && runningWish.task_id ? taskById(runCli, runningWish.task_id) : null;
|
|
381
|
+
const waitingForAnswer = runningWish && WAITING_WISH_STATUSES.has(String(runningWish.status || ''));
|
|
382
|
+
const result = waitingResult({
|
|
383
|
+
ask: text,
|
|
384
|
+
wish: runningWish,
|
|
385
|
+
mission: runningMission,
|
|
386
|
+
task: runningTask,
|
|
387
|
+
question: waitingForAnswer
|
|
388
|
+
? (Array.isArray(runningWish.questions) ? runningWish.questions[0] : '')
|
|
389
|
+
: 'an identical lap is already running',
|
|
390
|
+
nextAction: waitingForAnswer
|
|
391
|
+
? wishAnswerCommand(runningWish)
|
|
392
|
+
: oneLapRetryCommand(text, options),
|
|
393
|
+
});
|
|
394
|
+
result.resumed = true;
|
|
395
|
+
emit(result, asJson);
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const existing = activeWishForAsk(root, text);
|
|
401
|
+
if (existing && WAITING_WISH_STATUSES.has(String(existing.wish.status || ''))) {
|
|
402
|
+
const result = waitingResult({
|
|
403
|
+
ask: text,
|
|
404
|
+
wish: existing.wish,
|
|
405
|
+
question: Array.isArray(existing.wish.questions) ? existing.wish.questions[0] : '',
|
|
406
|
+
nextAction: wishAnswerCommand(existing.wish),
|
|
407
|
+
});
|
|
408
|
+
result.resumed = true;
|
|
409
|
+
emit(result, asJson);
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let wish = existing && existing.wish;
|
|
414
|
+
let mission = existing && existing.mission;
|
|
415
|
+
let task = wish && wish.task_id ? taskById(runCli, wish.task_id) : null;
|
|
416
|
+
const retryVerifier = String(options.verifier || '').trim();
|
|
417
|
+
const existingVerifier = missionVerifier(mission);
|
|
418
|
+
if (existingVerifier && retryVerifier && retryVerifier !== existingVerifier) {
|
|
419
|
+
const result = {
|
|
420
|
+
...resultBase({ status: 'stuck', ask: text, wish, mission, task, verifier: existingVerifier }),
|
|
421
|
+
resumed: true,
|
|
422
|
+
reason: 'the lap verifier is frozen after intake and cannot be replaced on retry',
|
|
423
|
+
next_action: oneLapRetryCommand(text, { engine: options.engine || '', verifier: existingVerifier }),
|
|
424
|
+
};
|
|
425
|
+
emit(result, asJson);
|
|
426
|
+
return 2;
|
|
427
|
+
}
|
|
428
|
+
if (task && ['review', 'done'].includes(String(task.status || ''))) {
|
|
429
|
+
const verifier = missionVerifier(mission);
|
|
430
|
+
const prior = verifier ? findDispatchReceipt(root, task.display_id, {
|
|
431
|
+
wishId: wish.id,
|
|
432
|
+
missionId: mission && mission.id,
|
|
433
|
+
verifier,
|
|
434
|
+
}) : null;
|
|
435
|
+
if (!prior) {
|
|
436
|
+
const result = {
|
|
437
|
+
...resultBase({ status: 'stuck', ask: text, wish, mission, task, verifier }),
|
|
438
|
+
resumed: true,
|
|
439
|
+
reason: 'the matching task is in Review without a passing matching one-lap receipt',
|
|
440
|
+
next_action: `atris task show ${shellQuote(task.display_id)}`,
|
|
441
|
+
};
|
|
442
|
+
emit(result, asJson);
|
|
443
|
+
return 1;
|
|
444
|
+
}
|
|
445
|
+
const result = {
|
|
446
|
+
...resultBase({
|
|
447
|
+
status: 'done',
|
|
448
|
+
ask: text,
|
|
449
|
+
wish,
|
|
450
|
+
mission,
|
|
451
|
+
task,
|
|
452
|
+
engine: prior && prior.ready && prior.ready.engine
|
|
453
|
+
? { id: prior.ready.engine }
|
|
454
|
+
: (wish && wish.engine ? { id: wish.engine } : null),
|
|
455
|
+
verifier,
|
|
456
|
+
}),
|
|
457
|
+
resumed: true,
|
|
458
|
+
changed: task.title,
|
|
459
|
+
reason: 'the matching lap is already proof ready',
|
|
460
|
+
checked: `${verifier} passed`,
|
|
461
|
+
tested: 'the linked task is in Review',
|
|
462
|
+
receipt: prior && prior.path || '',
|
|
463
|
+
worktree: prior && prior.ready && prior.ready.worktree || null,
|
|
464
|
+
next_action: prior.ready.next_action || `atris task show ${shellQuote(task.display_id)}`,
|
|
465
|
+
mission_status: mission && mission.status || null,
|
|
466
|
+
approval_status: task.status === 'review' ? 'pending' : null,
|
|
467
|
+
master_changed: false,
|
|
468
|
+
...(prior && prior.receipt && prior.receipt.result ? { result: prior.receipt.result } : {}),
|
|
469
|
+
};
|
|
470
|
+
emit(result, asJson);
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
if (task && String(task.status || '') === 'claimed') {
|
|
474
|
+
const actor = String(mission && mission.owner || 'mission-lead').trim() || 'mission-lead';
|
|
475
|
+
if (String(task.claimed_by || '').toLowerCase() !== actor.toLowerCase()) {
|
|
476
|
+
const result = waitingResult({
|
|
477
|
+
ask: text,
|
|
478
|
+
wish,
|
|
479
|
+
mission,
|
|
480
|
+
task,
|
|
481
|
+
question: `the matching task is already held by ${task.claimed_by || 'another worker'}`,
|
|
482
|
+
nextAction: `atris task show ${shellQuote(task.display_id)}`,
|
|
483
|
+
});
|
|
484
|
+
result.resumed = true;
|
|
485
|
+
emit(result, asJson);
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
const released = runCli(['task', 'release', task.display_id, '--as', actor, '--json']);
|
|
489
|
+
if (!released || released.status !== 0) {
|
|
490
|
+
const detail = String(released && (released.stderr || released.stdout) || 'claim release failed').trim().slice(-300);
|
|
491
|
+
const result = {
|
|
492
|
+
...resultBase({ status: 'stuck', ask: text, wish, mission, task }),
|
|
493
|
+
resumed: true,
|
|
494
|
+
reason: `the abandoned one-lap claim could not be released: ${detail}`,
|
|
495
|
+
next_action: `atris task show ${shellQuote(task.display_id)}`,
|
|
496
|
+
};
|
|
497
|
+
emit(result, asJson);
|
|
498
|
+
return 1;
|
|
499
|
+
}
|
|
500
|
+
task = taskById(runCli, task.display_id);
|
|
501
|
+
}
|
|
502
|
+
if (!wish) {
|
|
503
|
+
const intakeSafetyIssue = oneLapSafetyIssue({ title: text, tag: 'wish' });
|
|
504
|
+
if (intakeSafetyIssue) {
|
|
505
|
+
// A one-lap only auto-dispatches safe local build/test/review/research
|
|
506
|
+
// work. Anything else (protected lane, outbound action, non-local intent)
|
|
507
|
+
// still has a front door: `atris wish` runs it through the gated flow.
|
|
508
|
+
// Hand the new person that exact command, not unactionable prose.
|
|
509
|
+
const result = { ...resultBase({ status: 'stuck', ask: text }), reason: intakeSafetyIssue, next_action: `atris wish ${shellQuote(text)}` };
|
|
510
|
+
emit(result, asJson);
|
|
511
|
+
return 2;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let executor;
|
|
516
|
+
try {
|
|
517
|
+
executor = readyExecutor(root, options.engine || '');
|
|
518
|
+
} catch (error) {
|
|
519
|
+
const result = { ...resultBase({ status: 'stuck', ask: text }), reason: error.message, next_action: 'atris engine' };
|
|
520
|
+
emit(result, asJson);
|
|
521
|
+
return 2;
|
|
522
|
+
}
|
|
523
|
+
if (!executor) {
|
|
524
|
+
const result = { ...resultBase({ status: 'stuck', ask: text }), reason: 'no ready executor engine is installed', next_action: 'atris engine' };
|
|
525
|
+
emit(result, asJson);
|
|
526
|
+
return 2;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!wish) {
|
|
530
|
+
const intakeVerifier = String(options.verifier || '').trim() || projectVerifier(root);
|
|
531
|
+
if (intakeVerifier) {
|
|
532
|
+
const parsedIntakeVerifier = parseVerifyCommand(intakeVerifier);
|
|
533
|
+
if (!parsedIntakeVerifier.ok) {
|
|
534
|
+
const result = { ...resultBase({ status: 'stuck', ask: text, engine: executor, verifier: intakeVerifier }), reason: `the verifier is not safe to run (${parsedIntakeVerifier.reason})`, next_action: 'choose a local test command' };
|
|
535
|
+
emit(result, asJson);
|
|
536
|
+
return 2;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
progress('lap: navigator is scoping the request');
|
|
540
|
+
const captured = runCli([
|
|
541
|
+
'wish', text,
|
|
542
|
+
'--engine', executor.id,
|
|
543
|
+
...(intakeVerifier ? ['--verify', intakeVerifier] : []),
|
|
544
|
+
'--one-lap',
|
|
545
|
+
'--json',
|
|
546
|
+
]);
|
|
547
|
+
const payload = parseJsonOutput(captured);
|
|
548
|
+
if (!payload) {
|
|
549
|
+
const detail = String(captured && (captured.stderr || captured.stdout) || 'wish intake failed').trim().slice(-500);
|
|
550
|
+
const result = { ...resultBase({ status: 'stuck', ask: text, engine: executor }), reason: detail, next_action: oneLapRetryCommand(text, { engine: executor.id, verifier: intakeVerifier }) };
|
|
551
|
+
emit(result, asJson);
|
|
552
|
+
return captured && captured.status === 2 ? 2 : 1;
|
|
553
|
+
}
|
|
554
|
+
const durableWish = readWishes(root).find((row) => row.id === payload.wish_id);
|
|
555
|
+
wish = durableWish || { id: payload.wish_id, task_id: payload.task_id, mission_id: payload.mission_id, engine: payload.engine, validator: payload.validator, questions: payload.questions, status: payload.status, text };
|
|
556
|
+
if (payload.status === 'needs_input') {
|
|
557
|
+
const result = waitingResult({
|
|
558
|
+
ask: text,
|
|
559
|
+
wish,
|
|
560
|
+
question: Array.isArray(payload.questions) ? payload.questions[0] : '',
|
|
561
|
+
nextAction: wishAnswerCommand(wish),
|
|
562
|
+
});
|
|
563
|
+
emit(result, asJson);
|
|
564
|
+
return 0;
|
|
565
|
+
}
|
|
566
|
+
if (payload.status !== 'delegated' || !payload.task_id || !payload.mission_id) {
|
|
567
|
+
const result = { ...resultBase({ status: 'stuck', ask: text, wish, engine: executor }), reason: 'wish intake did not produce one scoped task and mission', next_action: `atris wish show ${payload.wish_id}` };
|
|
568
|
+
emit(result, asJson);
|
|
569
|
+
return 2;
|
|
570
|
+
}
|
|
571
|
+
mission = listMissions(root).find((row) => row.id === payload.mission_id) || null;
|
|
572
|
+
task = taskById(runCli, payload.task_id);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (!mission || !task) {
|
|
576
|
+
const result = { ...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor }), reason: 'the scoped task or mission could not be loaded', next_action: wish && wish.id ? `atris wish show ${wish.id}` : 'atris wish list' };
|
|
577
|
+
emit(result, asJson);
|
|
578
|
+
return 1;
|
|
579
|
+
}
|
|
580
|
+
const safetyIssue = oneLapSafetyIssue(task);
|
|
581
|
+
if (safetyIssue) {
|
|
582
|
+
const result = { ...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor }), reason: safetyIssue, next_action: `atris task show ${task.display_id}` };
|
|
583
|
+
emit(result, asJson);
|
|
584
|
+
return 2;
|
|
585
|
+
}
|
|
586
|
+
let frozenVerifier = missionVerifier(mission);
|
|
587
|
+
const requestedVerifier = String(options.verifier || '').trim();
|
|
588
|
+
if (frozenVerifier && requestedVerifier && requestedVerifier !== frozenVerifier) {
|
|
589
|
+
const result = {
|
|
590
|
+
...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor, verifier: frozenVerifier }),
|
|
591
|
+
reason: 'the lap verifier is frozen after intake and cannot be replaced on retry',
|
|
592
|
+
next_action: oneLapRetryCommand(text, { engine: options.engine || '', verifier: frozenVerifier }),
|
|
593
|
+
};
|
|
594
|
+
emit(result, asJson);
|
|
595
|
+
return 2;
|
|
596
|
+
}
|
|
597
|
+
const verifier = frozenVerifier || requestedVerifier || projectVerifier(root);
|
|
598
|
+
if (!verifier) {
|
|
599
|
+
const result = waitingResult({
|
|
600
|
+
ask: text,
|
|
601
|
+
wish,
|
|
602
|
+
mission,
|
|
603
|
+
task,
|
|
604
|
+
question: 'what exact command proves this request works?',
|
|
605
|
+
nextAction: oneLapRetryCommand(text, { engine: options.engine || '', verifier: '<command>' }),
|
|
606
|
+
});
|
|
607
|
+
result.engine = executor.id;
|
|
608
|
+
emit(result, asJson);
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
611
|
+
const parsedVerifier = parseVerifyCommand(verifier);
|
|
612
|
+
if (!parsedVerifier.ok) {
|
|
613
|
+
const result = { ...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor, verifier }), reason: `the mission has no safe runnable verifier (${parsedVerifier.reason})`, next_action: `atris mission status ${mission.id}` };
|
|
614
|
+
emit(result, asJson);
|
|
615
|
+
return 2;
|
|
616
|
+
}
|
|
617
|
+
if (!frozenVerifier) {
|
|
618
|
+
try {
|
|
619
|
+
mission = freezeMissionVerifier(mission.id, verifier, root);
|
|
620
|
+
frozenVerifier = missionVerifier(mission);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
const result = {
|
|
623
|
+
...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor, verifier }),
|
|
624
|
+
reason: `the verifier could not be frozen before dispatch: ${error.message}`,
|
|
625
|
+
next_action: `atris mission status ${mission.id}`,
|
|
626
|
+
};
|
|
627
|
+
emit(result, asJson);
|
|
628
|
+
return 1;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const validators = readyValidators(root, wish && wish.validator || '', executor.id);
|
|
633
|
+
if (!validators.length) {
|
|
634
|
+
const result = {
|
|
635
|
+
...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor, verifier }),
|
|
636
|
+
reason: 'no distinct ready validator engine is installed',
|
|
637
|
+
next_action: 'atris engine',
|
|
638
|
+
};
|
|
639
|
+
emit(result, asJson);
|
|
640
|
+
return 2;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
progress(`lap: ${executor.id} is building ${task.display_id} in isolation`);
|
|
644
|
+
let flight;
|
|
645
|
+
try {
|
|
646
|
+
flight = await fleet.runDispatchFlight({
|
|
647
|
+
root,
|
|
648
|
+
taskIds: [task.display_id],
|
|
649
|
+
engine: executor.id,
|
|
650
|
+
reviewOnly: true,
|
|
651
|
+
verifierCommand: verifier,
|
|
652
|
+
validatorEngines: validators.map((entry) => entry.id),
|
|
653
|
+
actor: mission.owner || 'mission-lead',
|
|
654
|
+
installedEngines: engineRegistryView(root)
|
|
655
|
+
.filter((entry) => entry.health && entry.health.status === 'ready')
|
|
656
|
+
.filter((entry) => fleet.FLEET_CAPABLE.includes(entry.id))
|
|
657
|
+
.map((entry) => entry.id),
|
|
658
|
+
ownCli: runCli,
|
|
659
|
+
log: (line) => {
|
|
660
|
+
const clean = String(line || '')
|
|
661
|
+
.replace(/[—–]/g, '-')
|
|
662
|
+
.replace(/[✓✔]/g, 'passed')
|
|
663
|
+
.replace(/[✗✖]/g, 'failed')
|
|
664
|
+
.replace(/[⏸·→]/g, '-')
|
|
665
|
+
.trim();
|
|
666
|
+
if (clean) progress(`lap: ${clean.replace(/^[^a-z0-9]+/i, '')}`);
|
|
667
|
+
},
|
|
668
|
+
receiptContext: {
|
|
669
|
+
source: 'one_lap',
|
|
670
|
+
ask: text,
|
|
671
|
+
wish_id: wish.id,
|
|
672
|
+
mission_id: mission.id,
|
|
673
|
+
validator: wish.validator || null,
|
|
674
|
+
mission_room_receipt_path: mission.mission_room_receipt_path || mission.metadata && mission.metadata.mission_room_receipt_path || null,
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
} catch (error) {
|
|
678
|
+
const result = { ...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: executor, verifier }), reason: error.message || String(error), next_action: `atris task show ${task.display_id}` };
|
|
679
|
+
emit(result, asJson);
|
|
680
|
+
return 1;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const ready = flight.ready && flight.ready[0];
|
|
684
|
+
const receipt = relativeReceipt(root, flight.receipt);
|
|
685
|
+
if (!flight.result || flight.result.passed !== true || !ready) {
|
|
686
|
+
const paused = flight.paused && flight.paused[0] || {};
|
|
687
|
+
const actualEngine = paused.engine ? { id: paused.engine } : executor;
|
|
688
|
+
const validation = paused.validator_result || flight.result && flight.result.validator_result || null;
|
|
689
|
+
const verifierPassed = paused.verifier_result && paused.verifier_result.passed === true;
|
|
690
|
+
const result = {
|
|
691
|
+
...resultBase({
|
|
692
|
+
status: 'stuck',
|
|
693
|
+
ask: text,
|
|
694
|
+
wish,
|
|
695
|
+
mission,
|
|
696
|
+
task,
|
|
697
|
+
engine: actualEngine,
|
|
698
|
+
validator: validation && validation.engine ? { id: validation.engine } : null,
|
|
699
|
+
verifier,
|
|
700
|
+
}),
|
|
701
|
+
reason: paused.detail || `the lap paused at ${paused.stage || 'verification'}`,
|
|
702
|
+
checked: paused.verifier_result
|
|
703
|
+
? `${verifier} ${verifierPassed ? 'passed' : 'failed'} (exit ${paused.verifier_result.status})`
|
|
704
|
+
: '',
|
|
705
|
+
tested: 'the isolated change did not clear every proof gate',
|
|
706
|
+
receipt,
|
|
707
|
+
worktree: paused.worktree || null,
|
|
708
|
+
next_action: paused.worktree ? `cd ${shellQuote(paused.worktree)} && atris worktree guard` : `atris task show ${shellQuote(task.display_id)}`,
|
|
709
|
+
};
|
|
710
|
+
emit(result, asJson);
|
|
711
|
+
return 1;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
mission = markMissionReviewReady(mission.id, {
|
|
716
|
+
verifier,
|
|
717
|
+
receiptPath: flight.receipt,
|
|
718
|
+
taskId: task.display_id,
|
|
719
|
+
worktree: ready.worktree,
|
|
720
|
+
nextAction: ready.next_action,
|
|
721
|
+
}, root);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
const result = {
|
|
724
|
+
...resultBase({ status: 'stuck', ask: text, wish, mission, task, engine: { id: ready.engine }, verifier }),
|
|
725
|
+
reason: `proof passed, but mission state could not move to Review: ${error.message}`,
|
|
726
|
+
checked: `${verifier} passed (exit 0)`,
|
|
727
|
+
receipt,
|
|
728
|
+
worktree: ready.worktree,
|
|
729
|
+
next_action: `atris mission status ${mission.id}`,
|
|
730
|
+
};
|
|
731
|
+
emit(result, asJson);
|
|
732
|
+
return 1;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
task = { ...task, status: 'review' };
|
|
736
|
+
const verifyOutput = String(ready.verifier_result && ready.verifier_result.output || '').trim().replace(/\s+/g, ' ').slice(-300);
|
|
737
|
+
const result = {
|
|
738
|
+
...resultBase({
|
|
739
|
+
status: 'done',
|
|
740
|
+
ask: text,
|
|
741
|
+
wish,
|
|
742
|
+
mission,
|
|
743
|
+
task,
|
|
744
|
+
engine: { id: ready.engine },
|
|
745
|
+
validator: ready.validator_result && ready.validator_result.engine ? { id: ready.validator_result.engine } : null,
|
|
746
|
+
verifier,
|
|
747
|
+
}),
|
|
748
|
+
changed: task.title,
|
|
749
|
+
reason: 'the requested change is built and proof ready without changing master',
|
|
750
|
+
checked: `${verifier} passed (exit 0)`,
|
|
751
|
+
tested: verifyOutput || 'the verifier completed with no output',
|
|
752
|
+
receipt,
|
|
753
|
+
worktree: ready.worktree,
|
|
754
|
+
next_action: ready.next_action,
|
|
755
|
+
mission_status: mission.status,
|
|
756
|
+
approval_status: 'pending',
|
|
757
|
+
master_changed: false,
|
|
758
|
+
result: flight.result,
|
|
759
|
+
};
|
|
760
|
+
emit(result, asJson);
|
|
761
|
+
return 0;
|
|
762
|
+
} finally {
|
|
763
|
+
releaseAskLock(lapLock);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
module.exports = {
|
|
768
|
+
OUTBOUND_ACTION,
|
|
769
|
+
activeWishForAsk,
|
|
770
|
+
normalizedAsk,
|
|
771
|
+
oneLapSafetyIssue,
|
|
772
|
+
projectVerifier,
|
|
773
|
+
readyExecutor,
|
|
774
|
+
readyValidators,
|
|
775
|
+
runOneLap,
|
|
776
|
+
};
|