atris 3.79.0 → 3.80.1
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/README.md +6 -0
- package/atris/atris.md +66 -167
- package/atris/skills/security-audit/SKILL.md +77 -0
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris.md +66 -167
- package/bin/atris.js +6 -0
- package/commands/auth.js +2 -2
- package/commands/business.js +44 -1984
- package/commands/computer.js +165 -2412
- package/commands/member.js +189 -966
- package/commands/mission.js +229 -1409
- package/commands/pack.js +112 -1476
- package/commands/security-review.js +129 -0
- package/commands/site-deploy.js +5 -1
- package/commands/task.js +137 -1359
- package/lib/business-core.js +2022 -0
- package/lib/computer-core.js +2447 -0
- package/lib/engine-ask.js +1 -0
- package/lib/handoff-wake.js +8 -6
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +21 -2
- package/lib/member-board.js +12 -10
- package/lib/member-core.js +1014 -0
- package/lib/member-up.js +46 -3
- package/lib/mission-core.js +1559 -0
- package/lib/pack-core.js +1588 -0
- package/lib/security-audit-classes.js +318 -0
- package/lib/security-audit.js +1549 -0
- package/lib/task-core.js +1412 -0
- package/lib/task-decision.js +66 -0
- package/lib/task-explanation.js +20 -0
- package/lib/yes-lock.js +1 -1
- package/package.json +9 -2
- package/utils/auth.js +18 -4
package/commands/mission.js
CHANGED
|
@@ -115,318 +115,122 @@ const {
|
|
|
115
115
|
prepareMissionGitGuard,
|
|
116
116
|
unreadableMissionGuard,
|
|
117
117
|
} = require('../lib/mission-protected-lane');
|
|
118
|
-
|
|
119
|
-
const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
|
|
120
|
-
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
121
118
|
// Campaign hooks load on first use, so missions without a campaign never open its store.
|
|
122
119
|
const campaignMissions = () => require('../lib/campaign-missions');
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return gates;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function missionRouteScopePresent(leg) {
|
|
241
|
-
const files = Array.isArray(leg?.files) ? leg.files : leg?.files ? [leg.files] : [];
|
|
242
|
-
const surfaces = Array.isArray(leg?.surface) ? leg.surface : leg?.surface ? [leg.surface] : [];
|
|
243
|
-
return [...files, ...surfaces].some((value) => String(value || '').trim());
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function missionRouteCycle(legs) {
|
|
247
|
-
const ids = legs.map(missionRouteLegId);
|
|
248
|
-
const dependencies = new Map(legs.map((leg, index) => [
|
|
249
|
-
ids[index],
|
|
250
|
-
Array.isArray(leg.dependencies) ? leg.dependencies.map((value) => String(value || '').trim()).filter(Boolean) : [],
|
|
251
|
-
]));
|
|
252
|
-
const visiting = new Set();
|
|
253
|
-
const visited = new Set();
|
|
254
|
-
const stack = [];
|
|
255
|
-
|
|
256
|
-
function visit(id) {
|
|
257
|
-
if (visiting.has(id)) {
|
|
258
|
-
const start = stack.indexOf(id);
|
|
259
|
-
return [...stack.slice(start), id];
|
|
260
|
-
}
|
|
261
|
-
if (visited.has(id)) return null;
|
|
262
|
-
visiting.add(id);
|
|
263
|
-
stack.push(id);
|
|
264
|
-
for (const dependency of dependencies.get(id) || []) {
|
|
265
|
-
const cycle = visit(dependency);
|
|
266
|
-
if (cycle) return cycle;
|
|
267
|
-
}
|
|
268
|
-
stack.pop();
|
|
269
|
-
visiting.delete(id);
|
|
270
|
-
visited.add(id);
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
for (const id of ids) {
|
|
275
|
-
const cycle = visit(id);
|
|
276
|
-
if (cycle) return cycle;
|
|
277
|
-
}
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function validateMissionRouteProposal(proposal) {
|
|
282
|
-
const legs = proposal?.legs;
|
|
283
|
-
if (!Array.isArray(legs) || legs.length === 0) {
|
|
284
|
-
return { code: 'missing_legs', reason: 'route proposal must contain at least one leg' };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const ids = legs.map(missionRouteLegId);
|
|
288
|
-
const idSet = new Set(ids);
|
|
289
|
-
if (idSet.size !== ids.length) {
|
|
290
|
-
return { code: 'duplicate_leg_id', reason: 'route proposal contains duplicate leg ids' };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
for (let index = 0; index < legs.length; index += 1) {
|
|
294
|
-
const leg = legs[index];
|
|
295
|
-
const id = ids[index];
|
|
296
|
-
if (!leg || typeof leg !== 'object' || Array.isArray(leg)) {
|
|
297
|
-
return { code: 'invalid_leg', reason: `route leg ${id} must be an object` };
|
|
298
|
-
}
|
|
299
|
-
if (!String(leg.objective || '').trim()) {
|
|
300
|
-
return { code: 'missing_objective', reason: `route leg ${id} is missing an objective` };
|
|
301
|
-
}
|
|
302
|
-
if (!String(leg.verifier || '').trim()) {
|
|
303
|
-
return { code: 'missing_verifier', reason: `route leg ${id} is missing a verifier command` };
|
|
304
|
-
}
|
|
305
|
-
if (!String(leg.stop_condition || leg.terminal_condition || leg.terminal_state || '').trim()) {
|
|
306
|
-
return { code: 'missing_stop_condition', reason: `route leg ${id} is missing a stop or terminal condition` };
|
|
307
|
-
}
|
|
308
|
-
if (!missionRouteScopePresent(leg)) {
|
|
309
|
-
return { code: 'missing_scope', reason: `route leg ${id} has no files or surface scope` };
|
|
310
|
-
}
|
|
311
|
-
if (leg.dependencies !== undefined && !Array.isArray(leg.dependencies)) {
|
|
312
|
-
return { code: 'invalid_dependencies', reason: `route leg ${id} dependencies must be a list` };
|
|
313
|
-
}
|
|
314
|
-
for (const dependency of leg.dependencies || []) {
|
|
315
|
-
const dependencyId = String(dependency || '').trim();
|
|
316
|
-
if (!idSet.has(dependencyId)) {
|
|
317
|
-
return { code: 'unknown_dependency', reason: `route leg ${id} depends on unknown leg ${dependencyId || '(empty)'}` };
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const unsupportedGate = missionRouteHardGates(proposal)
|
|
323
|
-
.find((gate) => !SUPPORTED_MISSION_HARD_GATES.has(gate));
|
|
324
|
-
if (unsupportedGate) {
|
|
325
|
-
return { code: 'unsupported_hard_gate', reason: `route uses unsupported hard gate ${unsupportedGate}` };
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const cycle = missionRouteCycle(legs);
|
|
329
|
-
if (cycle) {
|
|
330
|
-
return { code: 'dependency_cycle', reason: `route dependencies form a cycle: ${cycle.join(' -> ')}` };
|
|
331
|
-
}
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function readMissionRouteProposal(args, asJson) {
|
|
336
|
-
const source = readFlag(args, '--file', '');
|
|
337
|
-
let raw = '';
|
|
338
|
-
try {
|
|
339
|
-
raw = source && source !== '-'
|
|
340
|
-
? fs.readFileSync(path.resolve(process.cwd(), source), 'utf8')
|
|
341
|
-
: fs.readFileSync(0, 'utf8');
|
|
342
|
-
} catch (error) {
|
|
343
|
-
exitMissionError(`Could not read route proposal: ${error.message}`, 1, asJson);
|
|
344
|
-
}
|
|
345
|
-
if (!String(raw || '').trim()) {
|
|
346
|
-
exitMissionError('Route proposal JSON is required on stdin or with --file <path>.', 1, asJson);
|
|
347
|
-
}
|
|
348
|
-
let parsed;
|
|
349
|
-
try {
|
|
350
|
-
parsed = JSON.parse(raw);
|
|
351
|
-
} catch (error) {
|
|
352
|
-
exitMissionError(`Route proposal is not valid JSON: ${error.message}`, 1, asJson);
|
|
353
|
-
}
|
|
354
|
-
if (Array.isArray(parsed)) return { legs: parsed, hard_gates: [] };
|
|
355
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
356
|
-
exitMissionError('Route proposal must be a list of legs or an object with a legs list.', 1, asJson);
|
|
357
|
-
}
|
|
358
|
-
return {
|
|
359
|
-
...parsed,
|
|
360
|
-
legs: parsed.legs,
|
|
361
|
-
hard_gates: parsed.hard_gates ?? [],
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const MISSION_NATIVE_RUNNER_NAMES = Object.freeze(['manual', 'claude', 'atris2', 'codex_goal', 'caller_session', 'current_agent', 'drill']);
|
|
366
|
-
const MISSION_NATIVE_RUNNER_SET = new Set(MISSION_NATIVE_RUNNER_NAMES);
|
|
367
|
-
const MISSION_AUTO_RUNNER = 'auto';
|
|
368
|
-
|
|
369
|
-
function canonicalEngineName(name) {
|
|
370
|
-
const trimmed = String(name || '').trim();
|
|
371
|
-
if (!trimmed) return '';
|
|
372
|
-
if (RUNNER_PROFILE_DEFS[trimmed]) return trimmed;
|
|
373
|
-
if (RUNNER_PROFILE_ALIASES[trimmed]) return RUNNER_PROFILE_ALIASES[trimmed];
|
|
374
|
-
return '';
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function knownMissionRunnerText() {
|
|
378
|
-
return `Known runners: ${[...MISSION_NATIVE_RUNNER_NAMES, MISSION_AUTO_RUNNER].join(', ')}. Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}`;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function resolveMissionRunnerSelection(value, options = {}) {
|
|
382
|
-
const raw = String(value || '').trim();
|
|
383
|
-
const asJson = options.asJson === true;
|
|
384
|
-
const engineOnly = options.engineOnly === true;
|
|
385
|
-
if (!raw) {
|
|
386
|
-
exitMissionError(`${options.label || 'runner'} is required. ${knownMissionRunnerText()}.`, 2, asJson);
|
|
387
|
-
}
|
|
388
|
-
const runner = raw.toLowerCase();
|
|
389
|
-
if (runner === MISSION_AUTO_RUNNER) {
|
|
390
|
-
return { requested: raw, runner, engine: null, kind: 'auto' };
|
|
391
|
-
}
|
|
392
|
-
if (!engineOnly && MISSION_NATIVE_RUNNER_SET.has(runner)) {
|
|
393
|
-
return { requested: raw, runner, engine: null, kind: 'runner' };
|
|
394
|
-
}
|
|
395
|
-
const engine = canonicalEngineName(raw);
|
|
396
|
-
if (engine) {
|
|
397
|
-
return { requested: raw, runner: engine, engine, kind: 'engine' };
|
|
398
|
-
}
|
|
399
|
-
const noun = engineOnly ? 'engine' : 'runner';
|
|
400
|
-
exitMissionError(`Unknown ${noun} "${raw}". ${knownMissionRunnerText()}.`, 2, asJson);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// The roster line's effort and time cap for this tick, when it sets them.
|
|
404
|
-
function rosterTickPins(engine) {
|
|
405
|
-
return {
|
|
406
|
-
...(engine && engine.roster_effort ? { roster_effort: engine.roster_effort } : {}),
|
|
407
|
-
...(engine && engine.roster_max_seconds ? { roster_max_seconds: engine.roster_max_seconds } : {}),
|
|
408
|
-
...(engine && engine.roster_prep ? { roster_prep: engine.roster_prep } : {}),
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// A tick whose roster worker asks for prep ("prep: search") runs the same
|
|
413
|
-
// prep pass fleet runs before the worker starts, and the worker's prompt
|
|
414
|
-
// gets the brief. No prep line means the prompt goes out as it was.
|
|
415
|
-
async function missionTickPrep(root, { mission, runtimeMission, job, prompt, ask } = {}) {
|
|
416
|
-
const prepJob = String(runtimeMission && runtimeMission.roster_prep || '').trim();
|
|
417
|
-
if (!prepJob) return { prompt, prep: null };
|
|
418
|
-
const { runPrepPass, withPrepBrief } = require('../lib/roster-prep');
|
|
419
|
-
const prep = await runPrepPass({
|
|
420
|
-
prepJob,
|
|
421
|
-
forJob: job || 'build',
|
|
422
|
-
task: { display_id: (mission && mission.id) || '', title: (mission && (mission.objective || mission.slug)) || '' },
|
|
423
|
-
prompt,
|
|
424
|
-
root,
|
|
425
|
-
record: { member: (mission && mission.owner) || '' },
|
|
426
|
-
...(ask ? { ask } : {}),
|
|
427
|
-
});
|
|
428
|
-
return { prompt: withPrepBrief(prompt, prep), prep };
|
|
429
|
-
}
|
|
120
|
+
const {
|
|
121
|
+
VALID_STATUSES,
|
|
122
|
+
TERMINAL_STATUSES,
|
|
123
|
+
GOAL_LOOP_STATUSES,
|
|
124
|
+
STATUS_ALIASES,
|
|
125
|
+
CODEX_NATIVE_GOAL_SLOT_STATUSES,
|
|
126
|
+
CODEX_NATIVE_GOAL_REPLACE_STATUSES,
|
|
127
|
+
CODEX_NATIVE_GOAL_CLOSED_STATUSES,
|
|
128
|
+
DEFAULT_LONG_RUN_VERIFIER,
|
|
129
|
+
SLEEP_LENGTH_BUDGET_SECONDS,
|
|
130
|
+
HUMAN_BLOCKING_PAUSE_REASONS,
|
|
131
|
+
MISSION_RUNNER_AUTH_EXPIRED_RE,
|
|
132
|
+
missionRunnerAuthExpired,
|
|
133
|
+
MISSION_BUDGET_TIERS,
|
|
134
|
+
stampIso,
|
|
135
|
+
todayName,
|
|
136
|
+
slugify,
|
|
137
|
+
shortHash,
|
|
138
|
+
missionId,
|
|
139
|
+
destinationHash,
|
|
140
|
+
missionDrive,
|
|
141
|
+
proposeMissionDestinationChange,
|
|
142
|
+
SUPPORTED_MISSION_HARD_GATES,
|
|
143
|
+
missionRouteLegId,
|
|
144
|
+
missionRouteHardGates,
|
|
145
|
+
missionRouteScopePresent,
|
|
146
|
+
missionRouteCycle,
|
|
147
|
+
validateMissionRouteProposal,
|
|
148
|
+
readMissionRouteProposal,
|
|
149
|
+
MISSION_NATIVE_RUNNER_NAMES,
|
|
150
|
+
MISSION_NATIVE_RUNNER_SET,
|
|
151
|
+
MISSION_AUTO_RUNNER,
|
|
152
|
+
canonicalEngineName,
|
|
153
|
+
knownMissionRunnerText,
|
|
154
|
+
resolveMissionRunnerSelection,
|
|
155
|
+
rosterTickPins,
|
|
156
|
+
missionTickPrep,
|
|
157
|
+
memberLogTickReason,
|
|
158
|
+
resolveMissionTickRunner,
|
|
159
|
+
recordMissionEngineTickOutcome,
|
|
160
|
+
missionTickRunOutcome,
|
|
161
|
+
recordMissionTickRosterRun,
|
|
162
|
+
runnerModelPatch,
|
|
163
|
+
missionRunRuntimeView,
|
|
164
|
+
applyMissionRunnerProfile,
|
|
165
|
+
CLAUDE_FAMILY_RUNNER_ENGINES,
|
|
166
|
+
resolveMissionTickRunnerModel,
|
|
167
|
+
exitMissionError,
|
|
168
|
+
warnIfSummaryNeedsOperatorWhy,
|
|
169
|
+
warnIfTaskTitleNeedsOperatorWhy,
|
|
170
|
+
readPositiveIntegerFlag,
|
|
171
|
+
readNonNegativeIntegerFlag,
|
|
172
|
+
readRepeatedFlag,
|
|
173
|
+
codexNativeGoalOptionsFromArgs,
|
|
174
|
+
lintMissionVerifier,
|
|
175
|
+
assertMissionVerifier,
|
|
176
|
+
stripKnownFlags,
|
|
177
|
+
wantsJson,
|
|
178
|
+
printJsonOrText,
|
|
179
|
+
MISSION_RUN_VALUE_FLAGS,
|
|
180
|
+
MISSION_RUN_BOOLEAN_FLAGS,
|
|
181
|
+
DEFAULT_MISSION_RUN_OWNER_SLUGS,
|
|
182
|
+
missionRunInputRequired,
|
|
183
|
+
askLine,
|
|
184
|
+
removeValueFlag,
|
|
185
|
+
missionRunOwnerRef,
|
|
186
|
+
missionRunArgsWithOwner,
|
|
187
|
+
missionRunInputFromArgs,
|
|
188
|
+
promptMissionRunInput,
|
|
189
|
+
loadTaskDb,
|
|
190
|
+
writeMissionTaskProjection,
|
|
191
|
+
missionTaskRef,
|
|
192
|
+
missionXpTaskTitle,
|
|
193
|
+
normalizeMissionTaskMatchText,
|
|
194
|
+
missionObjectiveTaskMatch,
|
|
195
|
+
taskRowToMissionXpBinding,
|
|
196
|
+
findOpenTaskMatchingMissionObjective,
|
|
197
|
+
statePaths,
|
|
198
|
+
readBusinessBinding,
|
|
199
|
+
businessIdForAtris2Mission,
|
|
200
|
+
readJsonLines,
|
|
201
|
+
missionLabel,
|
|
202
|
+
missionRef,
|
|
203
|
+
missionDisplayText,
|
|
204
|
+
missionNumberWinner,
|
|
205
|
+
MISSION_REF_SUFFIX_MIN_LENGTH,
|
|
206
|
+
uniqueMissionSuffix,
|
|
207
|
+
missionNumberDisplayIndex,
|
|
208
|
+
missionRefLooksLikeHandle,
|
|
209
|
+
missionMatchesRef,
|
|
210
|
+
missionsMatchingHandleFragment,
|
|
211
|
+
nearestMissionCandidates,
|
|
212
|
+
SIBLING_HINT_SCAN_LIMIT,
|
|
213
|
+
missionMatchesStatusFilter,
|
|
214
|
+
appendJsonLine,
|
|
215
|
+
resolveMemberWorkspaceRoot,
|
|
216
|
+
appendEvent,
|
|
217
|
+
memberDir,
|
|
218
|
+
appendMemberLog,
|
|
219
|
+
clipWords,
|
|
220
|
+
crystallizeMemberSkillLesson,
|
|
221
|
+
autoQueueMemberNextPush,
|
|
222
|
+
DEFAULT_MEMBER_BATON_RELAY,
|
|
223
|
+
recordMemberHandoff,
|
|
224
|
+
withHandoffsLock,
|
|
225
|
+
rewriteHandoffs,
|
|
226
|
+
updateMemberHandoff,
|
|
227
|
+
closeMemberHandoffsForReceiver,
|
|
228
|
+
closeHandoffRows,
|
|
229
|
+
memberMissionFile,
|
|
230
|
+
missingOwnerMemberWarning,
|
|
231
|
+
ensureMemberMissionFile,
|
|
232
|
+
removeLegacyGeneratedMissionViews,
|
|
233
|
+
} = require('../lib/mission-core');
|
|
430
234
|
|
|
431
235
|
// A tick's time limit: the time left on the mission, capped by the roster
|
|
432
236
|
// line's "max 20 min" when it sets one, else by the default tick cap.
|
|
@@ -446,559 +250,6 @@ function missionTickWallBound(mission, remainingMs) {
|
|
|
446
250
|
return remainingMs <= missionTickCapMs(mission);
|
|
447
251
|
}
|
|
448
252
|
|
|
449
|
-
// The reason a member log shows for a tick. A failed engine tick names its
|
|
450
|
-
// engine beside the reason, so a codex run that timed out stops counting as
|
|
451
|
-
// a claude timeout in the repeat-problem scan. The reason itself is left as
|
|
452
|
-
// is because engine health and backoff match on it.
|
|
453
|
-
function memberLogTickReason(result) {
|
|
454
|
-
const reason = result && result.reason;
|
|
455
|
-
if (!reason || !result.engine_id || result.status !== 'errored') return reason;
|
|
456
|
-
return `${reason} (engine: ${result.engine_id})`;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
460
|
-
if (String(mission && mission.runner || '').trim().toLowerCase() !== MISSION_AUTO_RUNNER) {
|
|
461
|
-
// A named engine still gets the roster's time limit for that engine.
|
|
462
|
-
const runner = String(mission && mission.runner || '').trim().toLowerCase();
|
|
463
|
-
const pin = runner && !(Number(mission && mission.roster_max_seconds) > 0)
|
|
464
|
-
? require('../lib/engine-registry').rosterPinFor('executor', runner, root, options)
|
|
465
|
-
: {};
|
|
466
|
-
const limited = pin.roster_max_seconds ? { ...mission, roster_max_seconds: pin.roster_max_seconds } : mission;
|
|
467
|
-
return { mission: limited, engine_id: null, requested_engine: null, engine_fallback_reason: null };
|
|
468
|
-
}
|
|
469
|
-
// A mission owned by a team member runs on that member's roster pick. An
|
|
470
|
-
// explicit preferred engine (or a run's --engine) still wins, and with no
|
|
471
|
-
// roster line behind the member, routing stays exactly as it was.
|
|
472
|
-
if (!String(mission.preferred_engine || '').trim() && mission.owner) {
|
|
473
|
-
const { memberRosterEngine } = require('../lib/member-engine');
|
|
474
|
-
const member = memberRosterEngine(mission.owner, root, options);
|
|
475
|
-
if (member) {
|
|
476
|
-
if (process.env.ATRIS_ROUTER_EXPLAIN !== '0') console.error(member.reason);
|
|
477
|
-
return {
|
|
478
|
-
mission: {
|
|
479
|
-
...mission,
|
|
480
|
-
runner: member.engine.id,
|
|
481
|
-
runner_kind: 'engine',
|
|
482
|
-
...(!mission.model && member.model ? { model: member.model } : {}),
|
|
483
|
-
...rosterTickPins(member.engine),
|
|
484
|
-
},
|
|
485
|
-
engine_id: member.engine.id,
|
|
486
|
-
requested_engine: null,
|
|
487
|
-
engine_fallback_reason: null,
|
|
488
|
-
member_engine_reason: member.reason,
|
|
489
|
-
roster_job: member.job || 'build',
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
const resolved = resolveEngineForRoleWithPreference('executor', root, mission.preferred_engine, options);
|
|
494
|
-
return {
|
|
495
|
-
mission: resolved.engine ? {
|
|
496
|
-
...mission,
|
|
497
|
-
runner: resolved.engine.id,
|
|
498
|
-
runner_kind: 'engine',
|
|
499
|
-
...(!mission.model && resolved.engine.roster_model ? { model: resolved.engine.roster_model } : {}),
|
|
500
|
-
...rosterTickPins(resolved.engine),
|
|
501
|
-
} : mission,
|
|
502
|
-
engine_id: resolved.engine ? resolved.engine.id : null,
|
|
503
|
-
requested_engine: resolved.requested_engine,
|
|
504
|
-
engine_fallback_reason: resolved.engine_fallback_reason,
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// A stalled tick benches its engine for the cooldown, so the next tick's
|
|
509
|
-
// roster pick is the backup; no retry inside the tick is needed.
|
|
510
|
-
function recordMissionEngineTickOutcome(engineId, result, root = process.cwd()) {
|
|
511
|
-
if (!engineId) return null;
|
|
512
|
-
if (result && result.status === 'ran') return setEngineHealth(engineId, 'ready', root);
|
|
513
|
-
if (!engineFailureHealthStatus(result)) return null;
|
|
514
|
-
return recordEngineRunHealth(engineId, result, root);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// One run-record line per engine tick: how long the worker ran, and how it
|
|
518
|
-
// ended. A tick that ran and passed (or had no verifier) landed; a failing
|
|
519
|
-
// verifier or a real error failed; a time cap or dropped connection stalled;
|
|
520
|
-
// a closed credit window is credit out. Never throws into the tick.
|
|
521
|
-
function missionTickRunOutcome(result, verifierResult, maxSeconds) {
|
|
522
|
-
if (result && result.status === 'ran') {
|
|
523
|
-
const review = verifierResult?.referee;
|
|
524
|
-
if (verifierResult?.passed === false && verifierResult.status === 0 && !verifierResult.timed_out
|
|
525
|
-
&& review?.mode === 'engine-unavailable' && review.reason !== 'code changed during review') {
|
|
526
|
-
return { outcome: 'review unavailable', detail: review.timed_out
|
|
527
|
-
? 'the build check passed, but the second review timed out'
|
|
528
|
-
: 'the build check passed, but the second review was unavailable' };
|
|
529
|
-
}
|
|
530
|
-
return verifierResult && verifierResult.passed === false
|
|
531
|
-
? { outcome: 'failed', detail: 'the verifier failed' }
|
|
532
|
-
: { outcome: 'landed' };
|
|
533
|
-
}
|
|
534
|
-
if (engineFailureHealthStatus(result) === 'credit_out') return { outcome: 'credit out', detail: 'the engine hit its usage limit' };
|
|
535
|
-
const stall = engineStallReason({ ...result, ...(maxSeconds ? { max_seconds: maxSeconds } : {}) });
|
|
536
|
-
if (stall) return { outcome: 'stalled', detail: stall.text };
|
|
537
|
-
if (/wall-exceeded/.test(String(result && result.reason || ''))) return { outcome: 'failed', detail: 'the mission ran out of time' };
|
|
538
|
-
const summary = String(result && result.claude && result.claude.summary || '').split('\n')[0].trim();
|
|
539
|
-
return { outcome: 'failed', detail: summary && summary !== 'error' ? summary : String(result && result.reason || 'the tick failed').replace(/-/g, ' ') };
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function recordMissionTickRosterRun(root, { mission, runtimeMission, engineId, job, result, verifierResult, startedAt, endedMs, usage, prep = null } = {}) {
|
|
543
|
-
if (!engineId) return null;
|
|
544
|
-
try {
|
|
545
|
-
const { engineRunsView } = require('../lib/roster-models');
|
|
546
|
-
const { prepRecordFields } = require('../lib/roster-prep');
|
|
547
|
-
const run = runtimeMission || mission || {};
|
|
548
|
-
const startedMs = Date.parse(startedAt);
|
|
549
|
-
const maxSeconds = Number(run.roster_max_seconds) || 0;
|
|
550
|
-
return appendRosterRun(root, {
|
|
551
|
-
at: startedAt,
|
|
552
|
-
job: job || 'build',
|
|
553
|
-
member: (mission && mission.owner) || '',
|
|
554
|
-
engine: engineId,
|
|
555
|
-
model: run.model || engineRunsView(engineId).model || '',
|
|
556
|
-
effort: run.roster_effort || '',
|
|
557
|
-
max_seconds: maxSeconds,
|
|
558
|
-
seconds: Number.isFinite(startedMs) ? Math.max(0, (endedMs - startedMs) / 1000) : undefined,
|
|
559
|
-
task: (mission && mission.id) || '',
|
|
560
|
-
source: 'mission',
|
|
561
|
-
...prepRecordFields(prep),
|
|
562
|
-
...(usage || {}),
|
|
563
|
-
...missionTickRunOutcome(result, verifierResult, maxSeconds),
|
|
564
|
-
});
|
|
565
|
-
} catch {
|
|
566
|
-
return null;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function runnerModelPatch(runner, model) {
|
|
571
|
-
const explicitModel = String(model || '').trim();
|
|
572
|
-
if (explicitModel) return { model: explicitModel };
|
|
573
|
-
if (String(runner || '').trim().toLowerCase() === 'atris2') return { model: 'atris:fast' };
|
|
574
|
-
return {};
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function missionRunRuntimeView(mission, runnerOverride = null, modelOverride = '') {
|
|
578
|
-
if (!runnerOverride) return mission;
|
|
579
|
-
const next = {
|
|
580
|
-
...mission,
|
|
581
|
-
runner: runnerOverride.runner,
|
|
582
|
-
runner_kind: runnerOverride.kind,
|
|
583
|
-
run_runner_override: {
|
|
584
|
-
requested: runnerOverride.requested,
|
|
585
|
-
runner: runnerOverride.runner,
|
|
586
|
-
stored_runner: mission.runner || null,
|
|
587
|
-
stored_model: mission.model || null,
|
|
588
|
-
},
|
|
589
|
-
};
|
|
590
|
-
delete next.model;
|
|
591
|
-
Object.assign(next, runnerModelPatch(runnerOverride.runner, modelOverride));
|
|
592
|
-
if (next.model) next.run_runner_override.model = next.model;
|
|
593
|
-
return next;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function applyMissionRunnerProfile(runner) {
|
|
597
|
-
const runnerName = String(runner || '').trim().toLowerCase();
|
|
598
|
-
// `manual` is the legacy name for a directly-run Claude mission. Do not let
|
|
599
|
-
// the host process's runner profile silently turn it into another engine.
|
|
600
|
-
const engine = canonicalEngineName(runnerName) || (runnerName === 'manual' ? 'claude' : '');
|
|
601
|
-
if (!engine) return () => {};
|
|
602
|
-
const previous = process.env.ATRIS_RUNNER_PROFILE;
|
|
603
|
-
process.env.ATRIS_RUNNER_PROFILE = engine;
|
|
604
|
-
return () => {
|
|
605
|
-
if (previous === undefined) delete process.env.ATRIS_RUNNER_PROFILE;
|
|
606
|
-
else process.env.ATRIS_RUNNER_PROFILE = previous;
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Claude-family engines share the claude CLI, so they keep the resolved
|
|
611
|
-
// default model pin. Every other engine's CLI rejects a claude model name, so
|
|
612
|
-
// it only ever takes an explicit pin (the mission's model or the roster
|
|
613
|
-
// line's); '' lets the engine ride its own default.
|
|
614
|
-
// A mission often still carries the claude model it was created with; an
|
|
615
|
-
// engine that runs only its own models (codex, grok) drops it.
|
|
616
|
-
const CLAUDE_FAMILY_RUNNER_ENGINES = new Set(['claude', 'fable', 'haiku']);
|
|
617
|
-
|
|
618
|
-
function resolveMissionTickRunnerModel(mission) {
|
|
619
|
-
const engine = canonicalEngineName(mission && mission.runner);
|
|
620
|
-
if (!engine || CLAUDE_FAMILY_RUNNER_ENGINES.has(engine)) {
|
|
621
|
-
return resolveClaudeRunnerModel(mission);
|
|
622
|
-
}
|
|
623
|
-
return runnerModelFor(engine, resolvePinnedRunnerModel(mission));
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function exitMissionError(message, code = 1, asJson = false) {
|
|
627
|
-
if (asJson) {
|
|
628
|
-
printCliJson(
|
|
629
|
-
{
|
|
630
|
-
ok: false,
|
|
631
|
-
error: message,
|
|
632
|
-
reason: 'mission_error',
|
|
633
|
-
detail: message,
|
|
634
|
-
selected_ref: null,
|
|
635
|
-
next_command: null,
|
|
636
|
-
},
|
|
637
|
-
compactErrorPayload({ reason: 'mission_error', detail: message }),
|
|
638
|
-
process.argv.slice(2),
|
|
639
|
-
);
|
|
640
|
-
} else {
|
|
641
|
-
console.error(message);
|
|
642
|
-
}
|
|
643
|
-
process.exit(code);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Write-time warnings judge only what a machine can truly judge: identifiers,
|
|
647
|
-
// flags, and task ids. Whether a why is present is a judgment call, the tick
|
|
648
|
-
// prompt demands it, the review pass judges it. A warning that cries wolf on
|
|
649
|
-
// plain sentences teaches agents to ignore it (golden-path papercut). The
|
|
650
|
-
// strict operatorReady bar stays at the digest surface, where under-showing
|
|
651
|
-
// is cheap. Boundary pinned by the marker-free fixture in mission-status tests.
|
|
652
|
-
function warnIfSummaryNeedsOperatorWhy(summary) {
|
|
653
|
-
const text = String(summary || '').trim();
|
|
654
|
-
if (!text || !hasAgentJargon(text)) return null;
|
|
655
|
-
const warning = 'Warning: this tick summary contains flags, ids, or code identifiers. Rewrite it in words the operator can use; identifiers belong in the receipt body.';
|
|
656
|
-
console.error(warning);
|
|
657
|
-
return warning;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function warnIfTaskTitleNeedsOperatorWhy(title) {
|
|
661
|
-
const text = String(title || '').trim();
|
|
662
|
-
if (!text || !hasAgentJargon(text)) return null;
|
|
663
|
-
const warning = 'Warning: this task title contains flags, ids, or code identifiers. Rewrite it in words the operator can use; identifiers belong in the task body.';
|
|
664
|
-
console.error(warning);
|
|
665
|
-
return warning;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function readPositiveIntegerFlag(args, name, fallback = null, options = {}) {
|
|
669
|
-
const raw = readFlag(args, name, '');
|
|
670
|
-
if (!raw) return fallback;
|
|
671
|
-
const value = Number(raw);
|
|
672
|
-
if (!Number.isInteger(value) || value < 1) {
|
|
673
|
-
exitMissionError(`${name} must be a positive integer`, 2, options.json);
|
|
674
|
-
}
|
|
675
|
-
return value;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function readNonNegativeIntegerFlag(args, name, fallback = null) {
|
|
679
|
-
const raw = readFlag(args, name, '');
|
|
680
|
-
if (!raw) return fallback;
|
|
681
|
-
const value = Number(raw);
|
|
682
|
-
if (!Number.isInteger(value) || value < 0) {
|
|
683
|
-
throw new Error(`${name} must be a non-negative integer.`);
|
|
684
|
-
}
|
|
685
|
-
return value;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function readRepeatedFlag(args, name) {
|
|
689
|
-
const values = [];
|
|
690
|
-
const prefix = `${name}=`;
|
|
691
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
692
|
-
const arg = String(args[i]);
|
|
693
|
-
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) {
|
|
694
|
-
values.push(readFlag([name, args[i + 1]], name, ''));
|
|
695
|
-
i += 1;
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
if (arg.startsWith(prefix)) values.push(readFlag([arg], name, ''));
|
|
699
|
-
}
|
|
700
|
-
return values.filter(Boolean);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function codexNativeGoalOptionsFromArgs(args) {
|
|
704
|
-
const nativeGoalStatus = readFlag(args, '--native-goal-status', readFlag(args, '--visible-goal-status', ''));
|
|
705
|
-
const nativeGoalObjective = readFlag(args, '--native-goal-objective', readFlag(args, '--visible-goal-objective', ''));
|
|
706
|
-
return {
|
|
707
|
-
...(nativeGoalStatus ? { nativeGoalStatus } : {}),
|
|
708
|
-
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
709
|
-
...(hasFlag(args, '--manual-ack') ? { manualAck: true } : {}),
|
|
710
|
-
...(hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal') ? { allowNativeGoalSupersede: true } : {}),
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function lintMissionVerifier(command) {
|
|
715
|
-
const text = String(command || '').trim();
|
|
716
|
-
if (!text) return null;
|
|
717
|
-
const compact = text.replace(/\s+/g, ' ');
|
|
718
|
-
const staticNumericTest = /^test \d+ -(?:eq|ne|gt|ge|lt|le) \d+$/.test(compact)
|
|
719
|
-
|| /^\[ \d+ -(?:eq|ne|gt|ge|lt|le) \d+ \]$/.test(compact);
|
|
720
|
-
if (!staticNumericTest) return null;
|
|
721
|
-
return 'looks like shell substitution expanded before Atris received it; quote dynamic verifiers with single quotes';
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function assertMissionVerifier(command, asJson = false) {
|
|
725
|
-
const issue = lintMissionVerifier(command);
|
|
726
|
-
if (!issue) return;
|
|
727
|
-
exitMissionError(`Invalid --verify: ${issue}. Example: --verify 'test $(wc -l < atris/learnings.jsonl) -ge 478'`, 2, asJson);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function stripKnownFlags(args, valueNames, booleanNames = []) {
|
|
731
|
-
const valueSet = new Set(valueNames);
|
|
732
|
-
const booleanSet = new Set(['--full', '--verbose', ...booleanNames]);
|
|
733
|
-
const out = [];
|
|
734
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
735
|
-
const arg = String(args[i]);
|
|
736
|
-
const key = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
|
|
737
|
-
if (booleanSet.has(key)) continue;
|
|
738
|
-
if (valueSet.has(key)) {
|
|
739
|
-
if (!arg.includes('=') && args[i + 1] && !String(args[i + 1]).startsWith('--')) i += 1;
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
out.push(args[i]);
|
|
743
|
-
}
|
|
744
|
-
return out;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function wantsJson(args) {
|
|
748
|
-
return hasFlag(args, '--json');
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
function printJsonOrText(payload, lines, asJson) {
|
|
752
|
-
if (asJson) {
|
|
753
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
for (const line of lines) console.log(line);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
const MISSION_RUN_VALUE_FLAGS = [
|
|
760
|
-
'--slots',
|
|
761
|
-
'--pick',
|
|
762
|
-
'--max-ticks',
|
|
763
|
-
'--max-idle-ticks',
|
|
764
|
-
'--max-wall',
|
|
765
|
-
'--minutes',
|
|
766
|
-
'--hours',
|
|
767
|
-
'--cadence',
|
|
768
|
-
'--owner',
|
|
769
|
-
'--runner',
|
|
770
|
-
'--lane',
|
|
771
|
-
'--verify',
|
|
772
|
-
'--stop',
|
|
773
|
-
'--model',
|
|
774
|
-
'--engine',
|
|
775
|
-
'--referee-engine',
|
|
776
|
-
'--budget',
|
|
777
|
-
'--campaign',
|
|
778
|
-
'--reserve',
|
|
779
|
-
'--repo',
|
|
780
|
-
'--native-goal-status',
|
|
781
|
-
'--native-goal-objective',
|
|
782
|
-
'--visible-goal-status',
|
|
783
|
-
'--visible-goal-objective',
|
|
784
|
-
];
|
|
785
|
-
const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
786
|
-
'--json',
|
|
787
|
-
'--full',
|
|
788
|
-
'--verbose',
|
|
789
|
-
'--due',
|
|
790
|
-
'--detach',
|
|
791
|
-
'--headless',
|
|
792
|
-
'--self-drive',
|
|
793
|
-
'--fleet',
|
|
794
|
-
'--dry-run',
|
|
795
|
-
'--land',
|
|
796
|
-
'--no-claude',
|
|
797
|
-
'--no-verify',
|
|
798
|
-
'--referee',
|
|
799
|
-
'--referee-if-check-edited',
|
|
800
|
-
'--no-referee',
|
|
801
|
-
'--complete-on-pass',
|
|
802
|
-
'--no-drain',
|
|
803
|
-
'--create-next',
|
|
804
|
-
'--spend-full-budget',
|
|
805
|
-
'--use-whole-budget',
|
|
806
|
-
'--stop-when-done',
|
|
807
|
-
'--preflight',
|
|
808
|
-
'--no-preflight',
|
|
809
|
-
'--room-preflight',
|
|
810
|
-
'--no-room-preflight',
|
|
811
|
-
'--room-auto-run',
|
|
812
|
-
'--no-room-auto-run',
|
|
813
|
-
'--manual-ack',
|
|
814
|
-
'--allow-native-goal-supersede',
|
|
815
|
-
'--supersede-paused-native-goal',
|
|
816
|
-
'--take-goal-slot',
|
|
817
|
-
'--always-on',
|
|
818
|
-
'--xp-task',
|
|
819
|
-
'--agent-xp',
|
|
820
|
-
'--cloud',
|
|
821
|
-
];
|
|
822
|
-
const DEFAULT_MISSION_RUN_OWNER_SLUGS = new Set(
|
|
823
|
-
FUNCTIONAL_MEMBER_TOPICS.map(topic => normalizeOwnerSlug(topic.owner)),
|
|
824
|
-
);
|
|
825
|
-
|
|
826
|
-
function missionRunInputRequired(asJson = false, owner = '') {
|
|
827
|
-
const defaultOwner = normalizeOwnerSlug(owner || process.env.ATRIS_AGENT_ID || 'mission-lead') || 'mission-lead';
|
|
828
|
-
const error = 'mission run needs an interactive terminal; use atris mission run --due --headless or atris mission run <explicit-mission-id>.';
|
|
829
|
-
const payload = {
|
|
830
|
-
ok: false,
|
|
831
|
-
action: 'mission_input_required',
|
|
832
|
-
error,
|
|
833
|
-
prompt: 'What mission should Atris run?',
|
|
834
|
-
owner: defaultOwner,
|
|
835
|
-
owner_prompt: 'Which team member should own it?',
|
|
836
|
-
example: `atris mission run "make onboarding magical" --owner ${defaultOwner}`,
|
|
837
|
-
};
|
|
838
|
-
if (asJson) {
|
|
839
|
-
console.log(JSON.stringify(payload));
|
|
840
|
-
} else {
|
|
841
|
-
console.error(error);
|
|
842
|
-
}
|
|
843
|
-
process.exit(1);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function askLine(rl, question) {
|
|
847
|
-
return new Promise((resolve) => rl.question(question, (answer) => resolve(String(answer || '').trim())));
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function removeValueFlag(args, name) {
|
|
851
|
-
const out = [];
|
|
852
|
-
const prefix = `${name}=`;
|
|
853
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
854
|
-
const arg = String(args[i]);
|
|
855
|
-
if (arg === name) {
|
|
856
|
-
if (args[i + 1] && !String(args[i + 1]).startsWith('--')) i += 1;
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
|
-
if (arg.startsWith(prefix)) continue;
|
|
860
|
-
out.push(args[i]);
|
|
861
|
-
}
|
|
862
|
-
return out;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function missionRunOwnerRef(ref, root = process.cwd()) {
|
|
866
|
-
const owner = normalizeOwnerSlug(ref);
|
|
867
|
-
if (!owner || /\s/.test(String(ref || ''))) return null;
|
|
868
|
-
if (listWorkspaceMemberSlugs(root).has(owner)) return owner;
|
|
869
|
-
if (DEFAULT_MISSION_RUN_OWNER_SLUGS.has(owner)) return owner;
|
|
870
|
-
return null;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
function missionRunArgsWithOwner(args, owner) {
|
|
874
|
-
return [...removeValueFlag(args, '--owner'), '--owner', owner];
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function missionRunInputFromArgs(args, root = process.cwd()) {
|
|
878
|
-
const positionals = stripKnownFlags(args, MISSION_RUN_VALUE_FLAGS, MISSION_RUN_BOOLEAN_FLAGS);
|
|
879
|
-
const explicitOwner = Boolean(readFlag(args, '--owner', ''));
|
|
880
|
-
if (!explicitOwner && positionals.length > 0) {
|
|
881
|
-
const owner = missionRunOwnerRef(positionals[0], root);
|
|
882
|
-
if (owner) {
|
|
883
|
-
return {
|
|
884
|
-
ref: positionals.slice(1).join(' ').trim(),
|
|
885
|
-
args: missionRunArgsWithOwner(args, owner),
|
|
886
|
-
owner,
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
return {
|
|
891
|
-
ref: positionals.join(' ').trim(),
|
|
892
|
-
args,
|
|
893
|
-
owner: readFlag(args, '--owner', ''),
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
async function promptMissionRunInput(args) {
|
|
898
|
-
if (!process.stdin.isTTY) missionRunInputRequired(wantsJson(args), readFlag(args, '--owner', ''));
|
|
899
|
-
const defaultOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
900
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
901
|
-
try {
|
|
902
|
-
const objective = await askLine(rl, 'Mission: ');
|
|
903
|
-
if (!objective) missionRunInputRequired(false);
|
|
904
|
-
const owner = await askLine(rl, `Team member [${defaultOwner}]: `);
|
|
905
|
-
return {
|
|
906
|
-
objective,
|
|
907
|
-
args: [...removeValueFlag(args, '--owner'), '--owner', owner || defaultOwner],
|
|
908
|
-
};
|
|
909
|
-
} finally {
|
|
910
|
-
rl.close();
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
function loadTaskDb(asJson = false) {
|
|
915
|
-
try {
|
|
916
|
-
return require('../lib/task-db');
|
|
917
|
-
} catch (error) {
|
|
918
|
-
const message = error && error.message ? error.message : String(error);
|
|
919
|
-
if (error?.code === 'ERR_UNKNOWN_BUILTIN_MODULE' || /node:sqlite/.test(message)) {
|
|
920
|
-
exitMissionError('AgentXP mission tasks require Node 22+ with node:sqlite.', 2, asJson);
|
|
921
|
-
}
|
|
922
|
-
throw error;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
function writeMissionTaskProjection(taskDb, db, workspaceRoot) {
|
|
927
|
-
const projection = taskDb.taskProjection(db, { workspaceRoot, limit: 500 });
|
|
928
|
-
const outPath = path.join(workspaceRoot, '.atris', 'state', 'tasks.projection.json');
|
|
929
|
-
writeJson(outPath, projection);
|
|
930
|
-
return { projection, outPath };
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
function missionTaskRef(task) {
|
|
934
|
-
return task?.display_id || task?.legacy_ref || String(task?.id || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 8);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function missionXpTaskTitle(objective) {
|
|
938
|
-
const prefix = 'Mission XP: ';
|
|
939
|
-
const normalized = String(objective || '').replace(/\s+/g, ' ').trim();
|
|
940
|
-
const boundary = normalized.search(/:|[.!?](?=\s|$)/);
|
|
941
|
-
const firstClause = (boundary > 0 ? normalized.slice(0, boundary) : normalized) || 'mission work';
|
|
942
|
-
const maxClauseLength = 64 - prefix.length;
|
|
943
|
-
if (firstClause.length <= maxClauseLength) return `${prefix}${firstClause}`;
|
|
944
|
-
const clipped = firstClause.slice(0, maxClauseLength + 1);
|
|
945
|
-
const wordBoundary = clipped.lastIndexOf(' ');
|
|
946
|
-
const shortClause = clipped.slice(0, wordBoundary > 0 ? wordBoundary : maxClauseLength).trim();
|
|
947
|
-
return `${prefix}${shortClause || 'mission work'}`;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
function normalizeMissionTaskMatchText(text) {
|
|
951
|
-
return String(text || '')
|
|
952
|
-
.replace(/^Mission XP:\s*/i, '')
|
|
953
|
-
.toLowerCase()
|
|
954
|
-
.replace(/\s+/g, ' ')
|
|
955
|
-
.trim();
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function missionObjectiveTaskMatch(objective, title) {
|
|
959
|
-
const want = normalizeMissionTaskMatchText(objective);
|
|
960
|
-
const have = normalizeMissionTaskMatchText(title);
|
|
961
|
-
if (!want || !have) return false;
|
|
962
|
-
if (want === have) return true;
|
|
963
|
-
// First clause of a long objective often matches the claimed task title.
|
|
964
|
-
const wantClause = want.split(/:|[.!?](?=\s|$)/)[0].trim();
|
|
965
|
-
const haveClause = have.split(/:|[.!?](?=\s|$)/)[0].trim();
|
|
966
|
-
if (wantClause && haveClause && (wantClause === haveClause || want.startsWith(haveClause) || have.startsWith(wantClause))) {
|
|
967
|
-
return true;
|
|
968
|
-
}
|
|
969
|
-
return false;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
function taskRowToMissionXpBinding(task, ownerResolution, outPath, { inserted = false, reused = true } = {}) {
|
|
973
|
-
return {
|
|
974
|
-
task_id: task.id,
|
|
975
|
-
ref: missionTaskRef(task) || task.id,
|
|
976
|
-
title: task.title,
|
|
977
|
-
operator_title_warning: warnIfTaskTitleNeedsOperatorWhy(task.title),
|
|
978
|
-
status: task.status || 'claimed',
|
|
979
|
-
assigned_to: task.claimed_by || ownerResolution.owner,
|
|
980
|
-
owner_resolution: ownerResolution.reason,
|
|
981
|
-
executed_by: ownerResolution.executed_by ? normalizeOwnerSlug(ownerResolution.executed_by) : null,
|
|
982
|
-
inserted,
|
|
983
|
-
reused,
|
|
984
|
-
projection_path: outPath,
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function findOpenTaskMatchingMissionObjective(mission, root = process.cwd(), asJson = false) {
|
|
989
|
-
const taskDb = loadTaskDb(asJson);
|
|
990
|
-
const db = taskDb.open();
|
|
991
|
-
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
992
|
-
const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot }));
|
|
993
|
-
const open = rows.filter((row) => row && ['open', 'claimed', 'review'].includes(row.status));
|
|
994
|
-
const explicitIds = Array.isArray(mission.task_ids) ? mission.task_ids.filter(Boolean) : [];
|
|
995
|
-
for (const id of explicitIds) {
|
|
996
|
-
const hit = open.find((row) => row.id === id || row.display_id === id || missionTaskRef(row) === id);
|
|
997
|
-
if (hit) return hit;
|
|
998
|
-
}
|
|
999
|
-
return open.find((row) => missionObjectiveTaskMatch(mission.objective, row.title)) || null;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
253
|
function bindExistingMissionTask(mission, task, root = process.cwd(), asJson = false) {
|
|
1003
254
|
const taskDb = loadTaskDb(asJson);
|
|
1004
255
|
const db = taskDb.open();
|
|
@@ -1072,80 +323,10 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
|
|
|
1072
323
|
};
|
|
1073
324
|
}
|
|
1074
325
|
|
|
1075
|
-
function statePaths(root = process.cwd()) {
|
|
1076
|
-
const stateDir = path.join(root, '.atris', 'state');
|
|
1077
|
-
return {
|
|
1078
|
-
stateDir,
|
|
1079
|
-
missionsJsonl: path.join(stateDir, 'missions.jsonl'),
|
|
1080
|
-
eventsJsonl: path.join(stateDir, 'mission_events.jsonl'),
|
|
1081
|
-
codexGoalJson: path.join(stateDir, 'codex_goal.json'),
|
|
1082
|
-
codexGoalRequestJson: path.join(stateDir, 'codex_goal_request.json'),
|
|
1083
|
-
codexGoalStatus: path.join(root, 'atris', 'status', 'codex-goal.md'),
|
|
1084
|
-
atrisGoalJson: path.join(stateDir, 'atris_goal.json'),
|
|
1085
|
-
atrisGoalStatus: path.join(root, 'atris', 'status', 'atris-goal.md'),
|
|
1086
|
-
statusNow: path.join(root, 'atris', 'status', 'now.md'),
|
|
1087
|
-
statusForYou: path.join(root, 'atris', 'status', 'for-you.md'),
|
|
1088
|
-
runsDir: path.join(root, 'atris', 'runs'),
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function readBusinessBinding(root = process.cwd()) {
|
|
1093
|
-
const file = path.join(root, '.atris', 'business.json');
|
|
1094
|
-
const parsed = readJson(file);
|
|
1095
|
-
if (parsed == null) return null;
|
|
1096
|
-
return {
|
|
1097
|
-
business_id: parsed.business_id || '',
|
|
1098
|
-
workspace_id: parsed.workspace_id || '',
|
|
1099
|
-
slug: parsed.slug || '',
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function businessIdForAtris2Mission(mission, cwd = process.cwd()) {
|
|
1104
|
-
return mission?.business_id || readBusinessBinding(cwd)?.business_id || null;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
function readJsonLines(file) {
|
|
1108
|
-
if (!fs.existsSync(file)) return [];
|
|
1109
|
-
return fs.readFileSync(file, 'utf8')
|
|
1110
|
-
.split(/\r?\n/)
|
|
1111
|
-
.map((line) => line.trim())
|
|
1112
|
-
.filter(Boolean)
|
|
1113
|
-
.map((line) => {
|
|
1114
|
-
try {
|
|
1115
|
-
return JSON.parse(line);
|
|
1116
|
-
} catch {
|
|
1117
|
-
return null;
|
|
1118
|
-
}
|
|
1119
|
-
})
|
|
1120
|
-
.filter(Boolean);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
326
|
function nextMissionNumber(root = process.cwd()) {
|
|
1124
327
|
return nextRecordNumber(canonicalMissionIndex(root));
|
|
1125
328
|
}
|
|
1126
329
|
|
|
1127
|
-
function missionLabel(mission) {
|
|
1128
|
-
const displayHandle = String(mission && mission.display_handle || '').trim();
|
|
1129
|
-
if (displayHandle) {
|
|
1130
|
-
const words = meaningfulWords(mission && (mission.objective || mission.slug || mission.id), 3);
|
|
1131
|
-
return `${displayHandle}${words.length ? ` ${words.join(' ')}` : ''}`;
|
|
1132
|
-
}
|
|
1133
|
-
return shortRecordLabel(mission, mission && (mission.objective || mission.slug || mission.id));
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
function missionRef(mission) {
|
|
1137
|
-
const displayHandle = String(mission && mission.display_handle || '').trim();
|
|
1138
|
-
if (displayHandle) return displayHandle;
|
|
1139
|
-
return shortRecordRef(mission);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
function missionDisplayText(mission, value) {
|
|
1143
|
-
const text = String(value || '');
|
|
1144
|
-
const id = String(mission && mission.id || '').trim();
|
|
1145
|
-
if (!id) return text;
|
|
1146
|
-
return text.split(id).join(missionRef(mission));
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
330
|
function assignMissionNumber(mission, root = process.cwd()) {
|
|
1150
331
|
if (!mission) return mission;
|
|
1151
332
|
const existing = mission.id ? loadMissionMap(root).get(mission.id) : null;
|
|
@@ -1417,107 +598,6 @@ function canonicalMissionIndex(root = process.cwd()) {
|
|
|
1417
598
|
return missions;
|
|
1418
599
|
}
|
|
1419
600
|
|
|
1420
|
-
function missionNumberWinner(records = []) {
|
|
1421
|
-
return records.reduce((winner, candidate) => {
|
|
1422
|
-
if (!winner) return candidate;
|
|
1423
|
-
const winnerTerminal = TERMINAL_STATUSES.has(winner.status);
|
|
1424
|
-
const candidateTerminal = TERMINAL_STATUSES.has(candidate.status);
|
|
1425
|
-
if (winnerTerminal !== candidateTerminal) return candidateTerminal ? winner : candidate;
|
|
1426
|
-
const winnerUpdated = String(winner.updated_at || winner.created_at || '');
|
|
1427
|
-
const candidateUpdated = String(candidate.updated_at || candidate.created_at || '');
|
|
1428
|
-
if (candidateUpdated !== winnerUpdated) return candidateUpdated > winnerUpdated ? candidate : winner;
|
|
1429
|
-
return String(candidate.id || '').localeCompare(String(winner.id || '')) < 0 ? candidate : winner;
|
|
1430
|
-
}, null);
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
function uniqueMissionSuffix(mission, missions) {
|
|
1434
|
-
const id = String(mission && mission.id || '').trim();
|
|
1435
|
-
if (!id) return 'unknown';
|
|
1436
|
-
const minimum = Math.min(MISSION_REF_SUFFIX_MIN_LENGTH, id.length);
|
|
1437
|
-
for (let length = Math.min(8, id.length); length <= id.length; length += 1) {
|
|
1438
|
-
const suffix = id.slice(-Math.max(length, minimum));
|
|
1439
|
-
if (missions.filter((candidate) => String(candidate.id || '').endsWith(suffix)).length === 1) return suffix;
|
|
1440
|
-
}
|
|
1441
|
-
return id;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
function missionNumberDisplayIndex(missions, canonicalMissions = missions) {
|
|
1445
|
-
const recordsByNumber = new Map();
|
|
1446
|
-
for (const mission of canonicalMissions) {
|
|
1447
|
-
const number = displayNumber(mission.n);
|
|
1448
|
-
if (!number) continue;
|
|
1449
|
-
const records = recordsByNumber.get(number) || [];
|
|
1450
|
-
records.push(mission);
|
|
1451
|
-
recordsByNumber.set(number, records);
|
|
1452
|
-
}
|
|
1453
|
-
const winnerIds = new Map();
|
|
1454
|
-
for (const [number, records] of recordsByNumber) {
|
|
1455
|
-
if (records.length > 1) winnerIds.set(number, missionNumberWinner(records)?.id);
|
|
1456
|
-
}
|
|
1457
|
-
return missions.map((mission) => {
|
|
1458
|
-
const number = displayNumber(mission.n);
|
|
1459
|
-
if (!number || !winnerIds.has(number) || winnerIds.get(number) === mission.id) return mission;
|
|
1460
|
-
return { ...mission, display_handle: uniqueMissionSuffix(mission, canonicalMissions) };
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
const MISSION_REF_SUFFIX_MIN_LENGTH = 6;
|
|
1465
|
-
|
|
1466
|
-
// A ref that "looks like" an id/suffix/number is a single token with no
|
|
1467
|
-
// whitespace matching a hex/id/numeric shape. Those must resolve or error,
|
|
1468
|
-
// never silently fall through to starting a new mission (a mistyped id/n
|
|
1469
|
-
// creating a junk mission is the worst failure mode here).
|
|
1470
|
-
function missionRefLooksLikeHandle(ref) {
|
|
1471
|
-
const raw = String(ref || '').trim();
|
|
1472
|
-
if (!raw || /\s/.test(raw)) return false;
|
|
1473
|
-
const withoutHash = raw.startsWith('#') ? raw.slice(1) : raw;
|
|
1474
|
-
if (/^\d+$/.test(withoutHash)) return true;
|
|
1475
|
-
if (/^mission-/.test(raw)) return true;
|
|
1476
|
-
if (/^[0-9a-f]{6,}$/i.test(withoutHash)) return true;
|
|
1477
|
-
return false;
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
function missionMatchesRef(mission, ref) {
|
|
1481
|
-
return mission && (recordMatchesRef(mission, ref) || mission.slug === String(ref || '').trim());
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
// Suffix/prefix id matches and slug-prefix matches, gated to unique hits of
|
|
1485
|
-
// at least MISSION_REF_SUFFIX_MIN_LENGTH chars so a short fragment can't
|
|
1486
|
-
// silently grab the wrong mission.
|
|
1487
|
-
function missionsMatchingHandleFragment(missions, ref) {
|
|
1488
|
-
const raw = String(ref || '').trim();
|
|
1489
|
-
if (!raw || raw.length < MISSION_REF_SUFFIX_MIN_LENGTH) return [];
|
|
1490
|
-
const lower = raw.toLowerCase();
|
|
1491
|
-
const idHits = missions.filter((mission) => {
|
|
1492
|
-
const id = String(mission.id || '').toLowerCase();
|
|
1493
|
-
return id.startsWith(lower) || id.endsWith(lower);
|
|
1494
|
-
});
|
|
1495
|
-
if (idHits.length) return idHits;
|
|
1496
|
-
return missions.filter((mission) => String(mission.slug || '').toLowerCase().startsWith(lower));
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
function nearestMissionCandidates(missions, ref, limit = 5) {
|
|
1500
|
-
const raw = String(ref || '').trim().toLowerCase();
|
|
1501
|
-
return missions
|
|
1502
|
-
.map((mission) => {
|
|
1503
|
-
const id = String(mission.id || '').toLowerCase();
|
|
1504
|
-
const slug = String(mission.slug || '').toLowerCase();
|
|
1505
|
-
let score = 0;
|
|
1506
|
-
if (raw && (id.includes(raw) || slug.includes(raw))) score = 2;
|
|
1507
|
-
else if (raw && raw.length >= 3 && (id.slice(-6).includes(raw.slice(0, 3)) || slug.includes(raw.slice(0, 3)))) score = 1;
|
|
1508
|
-
return { mission, score };
|
|
1509
|
-
})
|
|
1510
|
-
.filter((entry) => entry.score > 0)
|
|
1511
|
-
.sort((a, b) => b.score - a.score)
|
|
1512
|
-
.slice(0, limit)
|
|
1513
|
-
.map((entry) => entry.mission);
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
// Sibling folders looked at for a hint. A missing-id error waited 26 seconds
|
|
1517
|
-
// on a temp folder holding 276,000 entries, so the scan is capped and never
|
|
1518
|
-
// walks the system temp folder.
|
|
1519
|
-
const SIBLING_HINT_SCAN_LIMIT = 2000;
|
|
1520
|
-
|
|
1521
601
|
function listSiblingWorkspaceMissionHints(ref, root = process.cwd(), limit = 5, { scanLimit = SIBLING_HINT_SCAN_LIMIT, tmpRoot = os.tmpdir() } = {}) {
|
|
1522
602
|
if (!ref) return [];
|
|
1523
603
|
const baseRoot = path.resolve(root);
|
|
@@ -1663,58 +743,6 @@ function resolveMission(ref, root = process.cwd()) {
|
|
|
1663
743
|
return null;
|
|
1664
744
|
}
|
|
1665
745
|
|
|
1666
|
-
function missionMatchesStatusFilter(mission, statusFilter) {
|
|
1667
|
-
if (statusFilter === 'active') return !TERMINAL_STATUSES.has(mission.status);
|
|
1668
|
-
return mission.status === statusFilter;
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
function appendJsonLine(file, payload) {
|
|
1672
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1673
|
-
fs.appendFileSync(file, JSON.stringify(payload) + '\n', 'utf8');
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
function resolveMemberWorkspaceRoot(root = process.cwd()) {
|
|
1677
|
-
const resolved = path.resolve(root || process.cwd());
|
|
1678
|
-
const agentWorktreesMarker = `${path.sep}.agent-worktrees${path.sep}`;
|
|
1679
|
-
const agentIdx = resolved.indexOf(agentWorktreesMarker);
|
|
1680
|
-
if (agentIdx > 0) {
|
|
1681
|
-
const parentDir = resolved.slice(0, agentIdx);
|
|
1682
|
-
const after = resolved.slice(agentIdx + agentWorktreesMarker.length);
|
|
1683
|
-
const repoName = after.split(path.sep)[0];
|
|
1684
|
-
if (repoName) {
|
|
1685
|
-
const primaryRoot = path.join(parentDir, repoName);
|
|
1686
|
-
if (fs.existsSync(path.join(primaryRoot, 'atris'))) return primaryRoot;
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
const marker = `${path.sep}.atris${path.sep}worktrees${path.sep}`;
|
|
1690
|
-
const idx = resolved.indexOf(marker);
|
|
1691
|
-
if (idx > 0) {
|
|
1692
|
-
const parentRoot = resolved.slice(0, idx);
|
|
1693
|
-
if (fs.existsSync(path.join(parentRoot, 'atris'))) return parentRoot;
|
|
1694
|
-
}
|
|
1695
|
-
return resolved;
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
function appendEvent(type, mission, payload = {}, root = process.cwd()) {
|
|
1699
|
-
const paths = statePaths(root);
|
|
1700
|
-
const event = {
|
|
1701
|
-
schema: 'atris.mission_event.v1',
|
|
1702
|
-
type,
|
|
1703
|
-
mission_id: mission.id,
|
|
1704
|
-
at: stampIso(),
|
|
1705
|
-
actor: process.env.ATRIS_AGENT_ID || process.env.USER || null,
|
|
1706
|
-
payload,
|
|
1707
|
-
};
|
|
1708
|
-
appendJsonLine(paths.eventsJsonl, event);
|
|
1709
|
-
const parentRoot = resolveMemberWorkspaceRoot(root);
|
|
1710
|
-
if (type === 'mission_tick' && parentRoot !== path.resolve(root || process.cwd())) {
|
|
1711
|
-
try {
|
|
1712
|
-
appendJsonLine(statePaths(parentRoot).eventsJsonl, event);
|
|
1713
|
-
} catch {}
|
|
1714
|
-
}
|
|
1715
|
-
return event;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
746
|
function saveMission(mission, root = process.cwd(), eventType = 'mission_updated', payload = {}) {
|
|
1719
747
|
const paths = statePaths(root);
|
|
1720
748
|
const existing = mission && mission.id ? loadMissionMap(root).get(mission.id) : null;
|
|
@@ -1741,323 +769,6 @@ function saveMission(mission, root = process.cwd(), eventType = 'mission_updated
|
|
|
1741
769
|
return { mission: next, event };
|
|
1742
770
|
}
|
|
1743
771
|
|
|
1744
|
-
function memberDir(owner, root = process.cwd()) {
|
|
1745
|
-
if (!owner || !/^[a-zA-Z0-9._-]+$/.test(owner)) return null;
|
|
1746
|
-
const parentRoot = resolveMemberWorkspaceRoot(root);
|
|
1747
|
-
const candidates = parentRoot !== path.resolve(root || process.cwd())
|
|
1748
|
-
? [parentRoot, root]
|
|
1749
|
-
: [root];
|
|
1750
|
-
for (const base of candidates) {
|
|
1751
|
-
const dir = path.join(base, 'atris', 'team', owner);
|
|
1752
|
-
if (fs.existsSync(path.join(dir, 'MEMBER.md'))) return dir;
|
|
1753
|
-
}
|
|
1754
|
-
return null;
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
function appendMemberLog(owner, title, fields = {}, root = process.cwd()) {
|
|
1758
|
-
const dir = memberDir(owner, root);
|
|
1759
|
-
if (!dir) return null;
|
|
1760
|
-
const logsDir = path.join(dir, 'logs');
|
|
1761
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
1762
|
-
const logPath = path.join(logsDir, `${todayName()}.md`);
|
|
1763
|
-
const stamp = new Date().toTimeString().slice(0, 5);
|
|
1764
|
-
const rows = [
|
|
1765
|
-
`## ${stamp} · ${title}`,
|
|
1766
|
-
`- member: ${owner}`,
|
|
1767
|
-
...Object.entries(fields)
|
|
1768
|
-
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
1769
|
-
.map(([key, value]) => `- ${key}: ${String(value).replace(/\n/g, ' ')}`),
|
|
1770
|
-
'',
|
|
1771
|
-
];
|
|
1772
|
-
let lead = '';
|
|
1773
|
-
try {
|
|
1774
|
-
const size = fs.statSync(logPath).size;
|
|
1775
|
-
if (size > 0) {
|
|
1776
|
-
const fd = fs.openSync(logPath, 'r');
|
|
1777
|
-
const last = Buffer.alloc(1);
|
|
1778
|
-
fs.readSync(fd, last, 0, 1, size - 1);
|
|
1779
|
-
fs.closeSync(fd);
|
|
1780
|
-
if (last[0] !== 0x0a) lead = '\n';
|
|
1781
|
-
}
|
|
1782
|
-
} catch {}
|
|
1783
|
-
fs.appendFileSync(logPath, lead + rows.join('\n'), 'utf8');
|
|
1784
|
-
return logPath;
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// Shorten at a word boundary so a lesson never ends mid-word.
|
|
1788
|
-
function clipWords(text, max) {
|
|
1789
|
-
if (text.length <= max) return text;
|
|
1790
|
-
const cut = text.slice(0, max + 1);
|
|
1791
|
-
const space = cut.lastIndexOf(' ');
|
|
1792
|
-
return `${(space > max * 0.6 ? cut.slice(0, space) : text.slice(0, max)).replace(/[\s,;:.-]+$/, '')}...`;
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
function crystallizeMemberSkillLesson(owner, entry = {}, root = process.cwd()) {
|
|
1796
|
-
const dir = memberDir(owner, root);
|
|
1797
|
-
if (!dir) return null;
|
|
1798
|
-
const summary = String(entry.summary || entry.objective || '').replace(/\s+/g, ' ').trim();
|
|
1799
|
-
if (!summary) return null;
|
|
1800
|
-
const skillDir = path.join(dir, 'skills', 'auto-learned');
|
|
1801
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
1802
|
-
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
1803
|
-
let lines = null;
|
|
1804
|
-
if (fs.existsSync(skillPath)) {
|
|
1805
|
-
try {
|
|
1806
|
-
lines = fs.readFileSync(skillPath, 'utf8').split(/\r?\n/);
|
|
1807
|
-
} catch {}
|
|
1808
|
-
}
|
|
1809
|
-
const bulletAt = (lines || []).map((line, i) => (line.trim().startsWith('- **') ? i : -1)).filter((i) => i >= 0);
|
|
1810
|
-
const existingBullets = bulletAt.map((i) => lines[i].trim());
|
|
1811
|
-
const verifierText = String(entry.verifier || '').replace(/\s+/g, ' ').trim();
|
|
1812
|
-
const nextPushText = String(entry.next_push || '').replace(/\s+/g, ' ').trim();
|
|
1813
|
-
const layerLabel = String(entry.layer || 'capabilities').trim();
|
|
1814
|
-
const parts = [`- **${todayName()} (${layerLabel})**: ${clipWords(summary, 240)}`];
|
|
1815
|
-
// A cut-off command cannot be run, so a long one is left out, never clipped.
|
|
1816
|
-
if (verifierText && verifierText.length <= 300) parts.push(`Verify: \`${verifierText}\``);
|
|
1817
|
-
if (nextPushText) parts.push(`Next push: ${clipWords(nextPushText, 200)}`);
|
|
1818
|
-
const newBullet = parts.join(' -> ');
|
|
1819
|
-
const deduped = [
|
|
1820
|
-
newBullet,
|
|
1821
|
-
...existingBullets.filter((b) => !b.includes(summary.slice(0, 80))),
|
|
1822
|
-
].slice(0, 5);
|
|
1823
|
-
|
|
1824
|
-
let content;
|
|
1825
|
-
if (bulletAt.length) {
|
|
1826
|
-
// Keep whatever a person or reviewer wrote around the lessons; only the
|
|
1827
|
-
// lesson list itself is rewritten.
|
|
1828
|
-
const first = bulletAt[0];
|
|
1829
|
-
const last = bulletAt[bulletAt.length - 1];
|
|
1830
|
-
content = [...lines.slice(0, first), ...deduped, ...lines.slice(last + 1)].join('\n');
|
|
1831
|
-
} else {
|
|
1832
|
-
content = [
|
|
1833
|
-
'---',
|
|
1834
|
-
'name: auto-learned',
|
|
1835
|
-
`description: Verified patterns crystallized from passing runs for ${owner}. Bounded to 5 newest patterns so context never bloats.`,
|
|
1836
|
-
'---',
|
|
1837
|
-
'',
|
|
1838
|
-
'# Auto-Learned Verified Patterns',
|
|
1839
|
-
'',
|
|
1840
|
-
...deduped,
|
|
1841
|
-
'',
|
|
1842
|
-
].join('\n');
|
|
1843
|
-
}
|
|
1844
|
-
fs.writeFileSync(skillPath, content, 'utf8');
|
|
1845
|
-
return skillPath;
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
function autoQueueMemberNextPush(owner, nextPush, parentObjective = '', root = process.cwd(), { promote = false } = {}) {
|
|
1849
|
-
const cleanPush = String(nextPush || '').replace(/\s+/g, ' ').trim();
|
|
1850
|
-
if (!cleanPush) return null;
|
|
1851
|
-
const dir = memberDir(owner, root);
|
|
1852
|
-
if (!dir) return null;
|
|
1853
|
-
const goalsPath = path.join(dir, 'goals.json');
|
|
1854
|
-
let store = { schema_version: 1, member: owner, updated_at: stampIso(), goals: [] };
|
|
1855
|
-
if (fs.existsSync(goalsPath)) {
|
|
1856
|
-
try {
|
|
1857
|
-
const parsed = JSON.parse(fs.readFileSync(goalsPath, 'utf8'));
|
|
1858
|
-
if (parsed && Array.isArray(parsed.goals)) store = parsed;
|
|
1859
|
-
} catch {}
|
|
1860
|
-
}
|
|
1861
|
-
const now = stampIso();
|
|
1862
|
-
const activeIdx = store.goals.findIndex((g) => g && g.status === 'active');
|
|
1863
|
-
if (activeIdx >= 0) {
|
|
1864
|
-
const prev = store.goals[activeIdx];
|
|
1865
|
-
store.goals[activeIdx] = {
|
|
1866
|
-
...prev,
|
|
1867
|
-
...(promote ? { previous_title: prev.title, title: cleanPush } : {}),
|
|
1868
|
-
next_push: cleanPush,
|
|
1869
|
-
updated_at: now,
|
|
1870
|
-
};
|
|
1871
|
-
} else {
|
|
1872
|
-
store.goals.push({
|
|
1873
|
-
id: `goal_${Date.now().toString(36)}`,
|
|
1874
|
-
member: owner,
|
|
1875
|
-
title: cleanPush,
|
|
1876
|
-
why: parentObjective ? `Compounds after verified win on: ${parentObjective}` : 'Self-proposed next optimization from verified run.',
|
|
1877
|
-
metric: 'verified_slices_shipped',
|
|
1878
|
-
target: '1',
|
|
1879
|
-
horizon: '7d',
|
|
1880
|
-
acceptance: ['Passes test check and cross-model referee'],
|
|
1881
|
-
next_push: cleanPush,
|
|
1882
|
-
status: 'active',
|
|
1883
|
-
created_at: now,
|
|
1884
|
-
updated_at: now,
|
|
1885
|
-
});
|
|
1886
|
-
}
|
|
1887
|
-
store.updated_at = now;
|
|
1888
|
-
fs.writeFileSync(goalsPath, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
|
|
1889
|
-
return goalsPath;
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
const DEFAULT_MEMBER_BATON_RELAY = {
|
|
1893
|
-
researcher: 'navigator',
|
|
1894
|
-
'customer-lead': 'navigator',
|
|
1895
|
-
navigator: 'executor',
|
|
1896
|
-
architect: 'executor',
|
|
1897
|
-
executor: 'validator',
|
|
1898
|
-
'auto-improver': 'validator',
|
|
1899
|
-
};
|
|
1900
|
-
|
|
1901
|
-
function recordMemberHandoff(fromMember, toMember, sliceSummary, root = process.cwd(), extra = {}) {
|
|
1902
|
-
const from = String(fromMember || '').trim().toLowerCase();
|
|
1903
|
-
const to = String(toMember || DEFAULT_MEMBER_BATON_RELAY[from] || '').trim().toLowerCase();
|
|
1904
|
-
const cleanSummary = String(sliceSummary || '').replace(/\s+/g, ' ').trim();
|
|
1905
|
-
if (!from || !to || !cleanSummary) return null;
|
|
1906
|
-
const parentRoot = resolveMemberWorkspaceRoot(root);
|
|
1907
|
-
if (!memberDir(to, parentRoot)) return null;
|
|
1908
|
-
const now = stampIso();
|
|
1909
|
-
const handoff = {
|
|
1910
|
-
id: `handoff_${Date.now().toString(36)}`,
|
|
1911
|
-
from,
|
|
1912
|
-
to,
|
|
1913
|
-
summary: cleanSummary.slice(0, 180),
|
|
1914
|
-
status: extra.status || 'open',
|
|
1915
|
-
next_command: `atris member go ${to}`,
|
|
1916
|
-
created_at: now,
|
|
1917
|
-
};
|
|
1918
|
-
const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
|
|
1919
|
-
try {
|
|
1920
|
-
withHandoffsLock(handoffsFile, () => fs.appendFileSync(handoffsFile, `${JSON.stringify(handoff)}\n`, 'utf8'));
|
|
1921
|
-
} catch {
|
|
1922
|
-
return null;
|
|
1923
|
-
}
|
|
1924
|
-
try {
|
|
1925
|
-
autoQueueMemberNextPush(to, `${from} handoff: ${cleanSummary}`, `Baton pass from ${from}`, parentRoot);
|
|
1926
|
-
} catch {}
|
|
1927
|
-
return handoff;
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
// Every write to member-handoffs.jsonl (append or whole-file rewrite) holds
|
|
1931
|
-
// one lock file beside it, so concurrent handoffs never lose a row.
|
|
1932
|
-
// (Codex review round 3 on #1483.)
|
|
1933
|
-
function withHandoffsLock(handoffsFile, fn) {
|
|
1934
|
-
fs.mkdirSync(path.dirname(handoffsFile), { recursive: true });
|
|
1935
|
-
const lockFile = path.join(path.dirname(handoffsFile), 'member-handoffs.lock');
|
|
1936
|
-
return require('../lib/handoff-wake').withFileLock(lockFile, fn, { staleMs: 60 * 1000, waitMs: 30 * 1000 });
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
function rewriteHandoffs(handoffsFile, lines) {
|
|
1940
|
-
const tmp = `${handoffsFile}.${process.pid}.tmp`;
|
|
1941
|
-
fs.writeFileSync(tmp, `${lines.join('\n')}\n`, 'utf8');
|
|
1942
|
-
fs.renameSync(tmp, handoffsFile);
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
// Merge `patch` into the newest handoff row with this id. Returns the
|
|
1946
|
-
// updated row, or null when it is not on file.
|
|
1947
|
-
function updateMemberHandoff(handoffId, patch, root = process.cwd()) {
|
|
1948
|
-
const id = String(handoffId || '');
|
|
1949
|
-
if (!id || !patch) return null;
|
|
1950
|
-
const handoffsFile = path.join(resolveMemberWorkspaceRoot(root), '.atris', 'state', 'member-handoffs.jsonl');
|
|
1951
|
-
try {
|
|
1952
|
-
return withHandoffsLock(handoffsFile, () => {
|
|
1953
|
-
const lines = fs.readFileSync(handoffsFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
|
|
1954
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
1955
|
-
let row = null;
|
|
1956
|
-
try { row = JSON.parse(lines[i]); } catch {}
|
|
1957
|
-
if (!row || row.id !== id) continue;
|
|
1958
|
-
const updated = { ...row, ...patch };
|
|
1959
|
-
lines[i] = JSON.stringify(updated);
|
|
1960
|
-
rewriteHandoffs(handoffsFile, lines);
|
|
1961
|
-
return updated;
|
|
1962
|
-
}
|
|
1963
|
-
return null;
|
|
1964
|
-
});
|
|
1965
|
-
} catch {
|
|
1966
|
-
return null;
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
function closeMemberHandoffsForReceiver(receiverMember, root = process.cwd()) {
|
|
1971
|
-
const receiver = String(receiverMember || '').trim().toLowerCase();
|
|
1972
|
-
if (!receiver) return 0;
|
|
1973
|
-
const parentRoot = resolveMemberWorkspaceRoot(root);
|
|
1974
|
-
const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
|
|
1975
|
-
if (!fs.existsSync(handoffsFile)) return 0;
|
|
1976
|
-
try {
|
|
1977
|
-
return withHandoffsLock(handoffsFile, () => closeHandoffRows(handoffsFile, receiver));
|
|
1978
|
-
} catch {
|
|
1979
|
-
return 0;
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
function closeHandoffRows(handoffsFile, receiver) {
|
|
1984
|
-
const rawLines = fs.readFileSync(handoffsFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
|
|
1985
|
-
let closedCount = 0;
|
|
1986
|
-
const now = stampIso();
|
|
1987
|
-
const updatedLines = rawLines.map((line) => {
|
|
1988
|
-
try {
|
|
1989
|
-
const row = JSON.parse(line);
|
|
1990
|
-
if (row && String(row.to || '').toLowerCase() === receiver && (row.status || 'open') === 'open') {
|
|
1991
|
-
closedCount++;
|
|
1992
|
-
return JSON.stringify({ ...row, status: 'closed', closed_at: now });
|
|
1993
|
-
}
|
|
1994
|
-
} catch {}
|
|
1995
|
-
return line;
|
|
1996
|
-
});
|
|
1997
|
-
if (closedCount > 0) rewriteHandoffs(handoffsFile, updatedLines);
|
|
1998
|
-
return closedCount;
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
function memberMissionFile(owner, root = process.cwd()) {
|
|
2002
|
-
const dir = memberDir(owner, root);
|
|
2003
|
-
if (!dir) return null;
|
|
2004
|
-
return path.join(dir, 'MISSION.md');
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
function missingOwnerMemberWarning(owner, root = process.cwd()) {
|
|
2008
|
-
if (!owner || fs.existsSync(path.join(root, 'atris', 'team', owner))) return null;
|
|
2009
|
-
const teamDir = path.join(root, 'atris', 'team');
|
|
2010
|
-
const hasKnownMembers = fs.existsSync(teamDir)
|
|
2011
|
-
&& fs.readdirSync(teamDir, { withFileTypes: true }).some((entry) => (
|
|
2012
|
-
entry.isDirectory() && fs.existsSync(path.join(teamDir, entry.name, 'MEMBER.md'))
|
|
2013
|
-
));
|
|
2014
|
-
if (!hasKnownMembers) return null;
|
|
2015
|
-
return {
|
|
2016
|
-
code: 'missing_owner_member',
|
|
2017
|
-
message: `owner "${owner}" has no atris/team/${owner}/ member. Create it (atris member create ${owner}) or pick an existing member (ls atris/team/).`,
|
|
2018
|
-
};
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
function ensureMemberMissionFile(owner, root = process.cwd(), objective = '') {
|
|
2022
|
-
const missionPath = memberMissionFile(owner, root);
|
|
2023
|
-
if (!missionPath || fs.existsSync(missionPath)) return missionPath;
|
|
2024
|
-
const purpose = String(objective || '').trim() || 'Define why this member exists and how it chooses goals.';
|
|
2025
|
-
const content = [
|
|
2026
|
-
'# Mission',
|
|
2027
|
-
'',
|
|
2028
|
-
'<!-- Human-authored purpose file. Keep this durable; runtime state belongs in .atris/state/*.jsonl and now.md. -->',
|
|
2029
|
-
'',
|
|
2030
|
-
'## North Star',
|
|
2031
|
-
'',
|
|
2032
|
-
purpose,
|
|
2033
|
-
'',
|
|
2034
|
-
'## How To Choose Goals',
|
|
2035
|
-
'',
|
|
2036
|
-
'- Read MEMBER.md, MISSION.md, current goals, now.md, and recent logs.',
|
|
2037
|
-
'- Choose one useful bounded goal toward the mission.',
|
|
2038
|
-
'- Verify the work, write the receipt, and update the log.',
|
|
2039
|
-
'- Ask the human when vision, taste, risk, or uncertainty matters.',
|
|
2040
|
-
'',
|
|
2041
|
-
].join('\n');
|
|
2042
|
-
fs.writeFileSync(missionPath, content, 'utf8');
|
|
2043
|
-
return missionPath;
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
function removeLegacyGeneratedMissionViews(dir) {
|
|
2047
|
-
for (const name of ['missions.md', 'missions.json']) {
|
|
2048
|
-
const legacyPath = path.join(dir, name);
|
|
2049
|
-
if (!fs.existsSync(legacyPath)) continue;
|
|
2050
|
-
let text = '';
|
|
2051
|
-
try {
|
|
2052
|
-
text = fs.readFileSync(legacyPath, 'utf8');
|
|
2053
|
-
} catch {}
|
|
2054
|
-
const looksGenerated = name.endsWith('.json')
|
|
2055
|
-
? text.includes('"schema": "atris.member_missions.v1"')
|
|
2056
|
-
: text.includes('Generated from local Mission state');
|
|
2057
|
-
if (looksGenerated) fs.unlinkSync(legacyPath);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
772
|
// Metric-verified missions (wish --metric) render their target expression plus
|
|
2062
773
|
// the last observed value, read from the most recent verifier JSON line when a
|
|
2063
774
|
// tick has run the verifier (metric_verify.py prints {"value": ..., "proof_uri": ...}).
|
|
@@ -10003,8 +8714,91 @@ function missionDoneLooksLike(mission, root = process.cwd()) {
|
|
|
10003
8714
|
}
|
|
10004
8715
|
}
|
|
10005
8716
|
|
|
8717
|
+
function splitDoneLooksLikeClaims(doneLooksLike) {
|
|
8718
|
+
const line = String(doneLooksLike || '').replace(/\s+/g, ' ').trim().replace(/\.+$/, '').trim();
|
|
8719
|
+
if (!line) return [];
|
|
8720
|
+
const hasSerialJoin = /,\s+(?:and|then)\s+/i.test(line);
|
|
8721
|
+
const rawParts = line.split(/;\s*|\.\s+(?=[A-Za-z])|,\s+(?:and|then)\s+/i);
|
|
8722
|
+
const claims = [];
|
|
8723
|
+
for (const part of rawParts) {
|
|
8724
|
+
const trimmed = String(part || '').trim();
|
|
8725
|
+
if (!trimmed) continue;
|
|
8726
|
+
if (hasSerialJoin && trimmed.includes(', ')) {
|
|
8727
|
+
const sub = trimmed.split(/,\s+/).map((item) => item.trim()).filter(Boolean);
|
|
8728
|
+
if (sub.length > 1 && sub.every((item) => item.split(/\s+/).length >= 2)) {
|
|
8729
|
+
claims.push(...sub);
|
|
8730
|
+
continue;
|
|
8731
|
+
}
|
|
8732
|
+
}
|
|
8733
|
+
claims.push(trimmed);
|
|
8734
|
+
}
|
|
8735
|
+
return claims
|
|
8736
|
+
.map((claim) => claim.replace(/^(?:and|then)\s+/i, '').replace(/\.+$/, '').trim())
|
|
8737
|
+
.filter(Boolean);
|
|
8738
|
+
}
|
|
8739
|
+
|
|
8740
|
+
function collectVerifyEvidence(cwd, { diff = '', changedFiles = [], testOutput = '' } = {}) {
|
|
8741
|
+
let diffText = String(diff || '');
|
|
8742
|
+
const testText = String(testOutput || '');
|
|
8743
|
+
const files = new Set(
|
|
8744
|
+
(Array.isArray(changedFiles) ? changedFiles : [])
|
|
8745
|
+
.map((file) => String(file || '').trim())
|
|
8746
|
+
.filter(Boolean),
|
|
8747
|
+
);
|
|
8748
|
+
if (cwd && !diffText && !files.size) {
|
|
8749
|
+
try {
|
|
8750
|
+
const status = spawnSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd, encoding: 'utf8' });
|
|
8751
|
+
const statusLines = status.status === 0 ? String(status.stdout || '').split(/\r?\n/).filter(Boolean) : [];
|
|
8752
|
+
for (const line of statusLines) {
|
|
8753
|
+
const rel = line.slice(3).trim().replace(/^"(.*)"$/, '$1');
|
|
8754
|
+
if (!rel || /^(\.atris\/|atris\/(?:runs|status|logs)\/|atris\/team\/[^/]+\/logs\/)/.test(rel)) continue;
|
|
8755
|
+
files.add(rel);
|
|
8756
|
+
try {
|
|
8757
|
+
const full = path.join(cwd, rel);
|
|
8758
|
+
if (fs.statSync(full).isFile()) {
|
|
8759
|
+
diffText += `\n--- ${rel}\n${fs.readFileSync(full, 'utf8').slice(0, 8192)}`;
|
|
8760
|
+
}
|
|
8761
|
+
} catch {}
|
|
8762
|
+
}
|
|
8763
|
+
const patch = spawnSync('git', ['diff', 'HEAD'], { cwd, encoding: 'utf8' });
|
|
8764
|
+
if (patch.status === 0 && patch.stdout) diffText += `\n${patch.stdout}`;
|
|
8765
|
+
if (!files.size && !diffText.trim()) {
|
|
8766
|
+
const headShow = spawnSync('git', ['show', '--stat', '--patch', '--pretty=format:', 'HEAD'], { cwd, encoding: 'utf8' });
|
|
8767
|
+
if (headShow.status === 0 && headShow.stdout) diffText += `\n${headShow.stdout}`;
|
|
8768
|
+
}
|
|
8769
|
+
} catch {}
|
|
8770
|
+
}
|
|
8771
|
+
return { diffText, testText, files: [...files] };
|
|
8772
|
+
}
|
|
8773
|
+
|
|
8774
|
+
function missingDoneLooksLikeClaim(doneLooksLike, options = {}) {
|
|
8775
|
+
const reviewOutput = String(options.reviewOutput || '');
|
|
8776
|
+
const explicitMissing = reviewOutput.match(/VERDICT:\s*FAIL\s*[;:]\s*missing:\s*(.+)/i);
|
|
8777
|
+
if (explicitMissing) return explicitMissing[1].trim();
|
|
8778
|
+
const claims = splitDoneLooksLikeClaims(doneLooksLike);
|
|
8779
|
+
if (claims.length < 2) return '';
|
|
8780
|
+
const { diffText, testText, files } = collectVerifyEvidence(options.cwd, options);
|
|
8781
|
+
const haystack = [diffText, testText, ...files].filter(Boolean).join('\n');
|
|
8782
|
+
if (!haystack.trim()) return '';
|
|
8783
|
+
for (const claim of claims) {
|
|
8784
|
+
if (/\b(?:no|not|never|without|unchanged|untouched)\b/i.test(claim)) continue;
|
|
8785
|
+
const namedFiles = claim.match(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,8}\b|\b[A-Za-z0-9_-]+\.(?:txt|md|js|ts|tsx|jsx|json|py|sh|yml|yaml|sql|css|html|go|rs|csv)\b/g) || [];
|
|
8786
|
+
for (const file of namedFiles) {
|
|
8787
|
+
const base = path.basename(file);
|
|
8788
|
+
const touched = files.some((entry) => entry === file || path.basename(entry) === base)
|
|
8789
|
+
|| diffText.includes(file)
|
|
8790
|
+
|| diffText.includes(base)
|
|
8791
|
+
|| testText.includes(file)
|
|
8792
|
+
|| testText.includes(base);
|
|
8793
|
+
if (!touched) return claim;
|
|
8794
|
+
}
|
|
8795
|
+
}
|
|
8796
|
+
return '';
|
|
8797
|
+
}
|
|
8798
|
+
|
|
10006
8799
|
function buildEngineVerifyPrompt(mission, tickIndex, doneLooksLike = '', { timeoutMs = ENGINE_VERIFY_TIMEOUT_MS, checkPassed = '' } = {}) {
|
|
10007
8800
|
const doneLine = String(doneLooksLike || '').replace(/\s+/g, ' ').trim();
|
|
8801
|
+
const claims = splitDoneLooksLikeClaims(doneLine);
|
|
10008
8802
|
const minutes = Math.max(1, Math.round(timeoutMs / 60000));
|
|
10009
8803
|
return [
|
|
10010
8804
|
`# Verify Mission Tick ${tickIndex}`,
|
|
@@ -10013,8 +8807,9 @@ function buildEngineVerifyPrompt(mission, tickIndex, doneLooksLike = '', { timeo
|
|
|
10013
8807
|
...(doneLine ? [
|
|
10014
8808
|
`The job says done looks like: ${doneLine}`,
|
|
10015
8809
|
`Compare the work against that line. If the work does not show it, the verdict is FAIL even when the checks pass.`,
|
|
10016
|
-
`Split that line into each separate thing it promises and check every one. List each part as met or missing
|
|
10017
|
-
|
|
8810
|
+
`Split that line into each separate thing it promises and check every one. List each part as met or missing:`,
|
|
8811
|
+
...claims.map((claim, index) => `Claim ${index + 1}: ${claim}`),
|
|
8812
|
+
`Require evidence for each claim from the diff or test output. Work that does only some of the parts is not done. If any part is missing, end with 'VERDICT: FAIL; missing: <that part>'.`,
|
|
10018
8813
|
] : []),
|
|
10019
8814
|
// Member lab: reviewers spent about half their time on the workspace boot
|
|
10020
8815
|
// (atris atris.md, MAP, PERSONA, skills) before reading the change.
|
|
@@ -10027,11 +8822,11 @@ function buildEngineVerifyPrompt(mission, tickIndex, doneLooksLike = '', { timeo
|
|
|
10027
8822
|
...(checkPassed ? [`The mission check \`${checkPassed}\` already passed on this exact code. Do not run it again.`] : []),
|
|
10028
8823
|
`Prove the change with the narrowest real run: the changed command, or one test file or one named test.`,
|
|
10029
8824
|
`Do not run whole test suites or large test files; on a busy machine they outlast your time.`,
|
|
10030
|
-
`End your reply with a final line
|
|
8825
|
+
`End your reply with a final line 'VERDICT: PASS' or 'VERDICT: FAIL' (or 'VERDICT: FAIL; missing: <that part>' when a part of done-looks-like is unproven).`,
|
|
10031
8826
|
].join('\n');
|
|
10032
8827
|
}
|
|
10033
8828
|
|
|
10034
|
-
function engineVerifierResultFromRun(engineResult, engine = 'codex') {
|
|
8829
|
+
function engineVerifierResultFromRun(engineResult, engine = 'codex', options = {}) {
|
|
10035
8830
|
const output = String(
|
|
10036
8831
|
engineResult?.result
|
|
10037
8832
|
|| engineResult?.receipt_text
|
|
@@ -10049,6 +8844,21 @@ function engineVerifierResultFromRun(engineResult, engine = 'codex') {
|
|
|
10049
8844
|
};
|
|
10050
8845
|
}
|
|
10051
8846
|
const finalLine = output.trim().split(/\r?\n/).pop() || '';
|
|
8847
|
+
const missing = missingDoneLooksLikeClaim(options.doneLooksLike, {
|
|
8848
|
+
...options,
|
|
8849
|
+
reviewOutput: output,
|
|
8850
|
+
});
|
|
8851
|
+
if (missing) {
|
|
8852
|
+
const failLine = `VERDICT: FAIL; missing: ${missing}`;
|
|
8853
|
+
return {
|
|
8854
|
+
passed: false,
|
|
8855
|
+
mode: 'engine',
|
|
8856
|
+
engine,
|
|
8857
|
+
missing_claim: missing,
|
|
8858
|
+
reason: `missing: ${missing}`,
|
|
8859
|
+
output: /VERDICT:\s*FAIL/i.test(finalLine) ? output : `${output.trim()}\n${failLine}`.trim(),
|
|
8860
|
+
};
|
|
8861
|
+
}
|
|
10052
8862
|
return {
|
|
10053
8863
|
passed: finalLine === 'VERDICT: PASS',
|
|
10054
8864
|
mode: 'engine',
|
|
@@ -10133,6 +8943,7 @@ function engineVerifierLineup(mission, cwd, preferredEngine = '', options = {})
|
|
|
10133
8943
|
async function runEngineVerifier(mission, options = {}) {
|
|
10134
8944
|
const cwd = options.cwd || process.cwd();
|
|
10135
8945
|
const healthRoot = engineHealthRoot(cwd);
|
|
8946
|
+
const doneLooksLike = options.doneLooksLike !== undefined ? options.doneLooksLike : missionDoneLooksLike(mission, cwd);
|
|
10136
8947
|
const lineup = engineVerifierLineup(mission, cwd, options.refereeEngine, { healthRoot });
|
|
10137
8948
|
let verdict = null;
|
|
10138
8949
|
for (const { engine, model } of lineup) {
|
|
@@ -10146,7 +8957,7 @@ async function runEngineVerifier(mission, options = {}) {
|
|
|
10146
8957
|
cwd,
|
|
10147
8958
|
signal: options.signal,
|
|
10148
8959
|
timeoutMs: options.timeoutMs || ENGINE_VERIFY_TIMEOUT_MS,
|
|
10149
|
-
prompt: buildEngineVerifyPrompt(mission, options.tickIndex || 1,
|
|
8960
|
+
prompt: buildEngineVerifyPrompt(mission, options.tickIndex || 1, doneLooksLike, {
|
|
10150
8961
|
timeoutMs: ENGINE_VERIFY_TIMEOUT_MS,
|
|
10151
8962
|
checkPassed: options.checkPassed || '',
|
|
10152
8963
|
}),
|
|
@@ -10157,7 +8968,13 @@ async function runEngineVerifier(mission, options = {}) {
|
|
|
10157
8968
|
} finally {
|
|
10158
8969
|
restoreRunnerProfile();
|
|
10159
8970
|
}
|
|
10160
|
-
verdict = engineVerifierResultFromRun(engineResult, engine
|
|
8971
|
+
verdict = engineVerifierResultFromRun(engineResult, engine, {
|
|
8972
|
+
doneLooksLike,
|
|
8973
|
+
cwd,
|
|
8974
|
+
diff: options.diff,
|
|
8975
|
+
changedFiles: options.changedFiles,
|
|
8976
|
+
testOutput: options.testOutput,
|
|
8977
|
+
});
|
|
10161
8978
|
if (engineResult?.ok && !engineResult?.timedOut && !engineResult?.aborted) {
|
|
10162
8979
|
try { setEngineHealth(engine, 'ready', healthRoot); } catch {}
|
|
10163
8980
|
try { setEngineHealth(engine, 'ready', reviewerHealthRoot(engine)); } catch {}
|
|
@@ -11385,6 +10202,7 @@ async function verifyMissionRunTickPhase(context) {
|
|
|
11385
10202
|
tickIndex: currentTick.tickIdx,
|
|
11386
10203
|
refereeEngine: context.refereeEngine,
|
|
11387
10204
|
checkPassed: verifierResult?.passed ? currentTick.frozen.verifier : '',
|
|
10205
|
+
testOutput: [verifierResult?.stdout, verifierResult?.stderr].filter(Boolean).join('\n'),
|
|
11388
10206
|
});
|
|
11389
10207
|
const afterReview = gitCodeState(cwd);
|
|
11390
10208
|
const codeMoved = beforeReview.tree !== afterReview.tree
|
|
@@ -13103,6 +11921,8 @@ module.exports = {
|
|
|
13103
11921
|
missionVerifierHighLevelTestText,
|
|
13104
11922
|
buildEngineVerifyPrompt,
|
|
13105
11923
|
missionDoneLooksLike,
|
|
11924
|
+
splitDoneLooksLikeClaims,
|
|
11925
|
+
missingDoneLooksLikeClaim,
|
|
13106
11926
|
engineVerifierResultFromRun,
|
|
13107
11927
|
pickCrossEngineVerifier,
|
|
13108
11928
|
runVerifier,
|