atris 3.68.9 → 3.70.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/README.md +53 -0
- package/atris/team/opus-overnight/MEMBER.md +1 -0
- package/commands/init.js +9 -0
- package/commands/member.js +436 -40
- package/commands/mission.js +158 -12
- package/commands/sync.js +13 -0
- package/commands/task.js +63 -1
- package/commands/worktree.js +42 -5
- package/lib/auto-accept-certified.js +1 -0
- package/lib/engine-ask.js +2 -1
- package/lib/engine-registry.js +5 -4
- package/lib/engine-validate.js +189 -9
- package/lib/gitignore.js +56 -0
- package/lib/member-autoland.js +442 -0
- package/lib/member-board.js +257 -29
- package/lib/member-progress.js +68 -4
- package/lib/member-report.js +314 -0
- package/lib/member-scaffold.js +1 -2
- package/lib/member-switches.js +7 -0
- package/lib/member-task-pick.js +116 -0
- package/lib/member-tidy.js +235 -0
- package/lib/member-up.js +286 -0
- package/lib/member-verbs.js +84 -0
- package/lib/runner-command.js +29 -0
- package/lib/task-db.js +21 -1
- package/package.json +1 -1
package/commands/mission.js
CHANGED
|
@@ -8,7 +8,7 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
8
8
|
const { hasFlag, readFlag, readIntFlag } = require('../lib/arg-parser');
|
|
9
9
|
const { memberProcessPrompt } = require('../lib/member-context');
|
|
10
10
|
const { memberIdentityLine, missionObjectiveLine } = require('../lib/member-identity');
|
|
11
|
-
const { formatToolStep, appendProgress, reportProgress } = require('../lib/member-progress');
|
|
11
|
+
const { formatToolStep, formatCodexEvent, appendProgress, reportProgress, startedLine, newRunToken, runMarker } = require('../lib/member-progress');
|
|
12
12
|
const {
|
|
13
13
|
compactErrorPayload,
|
|
14
14
|
compactSuccessPayload,
|
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
resolveClaudeRunnerBin,
|
|
29
29
|
resolveClaudeRunnerCommandTemplate,
|
|
30
30
|
buildRunnerCommand,
|
|
31
|
+
codexJsonEventsCommand,
|
|
31
32
|
RUNNER_PROFILE_DEFS,
|
|
32
33
|
RUNNER_PROFILE_ALIASES,
|
|
33
34
|
RUNNER_PROFILE_NAMES,
|
|
@@ -433,7 +434,13 @@ function missionTickTimeoutMs(mission, remainingMs) {
|
|
|
433
434
|
|
|
434
435
|
function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
435
436
|
if (String(mission && mission.runner || '').trim().toLowerCase() !== MISSION_AUTO_RUNNER) {
|
|
436
|
-
|
|
437
|
+
// A named engine still gets the roster's time limit for that engine.
|
|
438
|
+
const runner = String(mission && mission.runner || '').trim().toLowerCase();
|
|
439
|
+
const pin = runner && !(Number(mission && mission.roster_max_seconds) > 0)
|
|
440
|
+
? require('../lib/engine-registry').rosterPinFor('executor', runner, root, options)
|
|
441
|
+
: {};
|
|
442
|
+
const limited = pin.roster_max_seconds ? { ...mission, roster_max_seconds: pin.roster_max_seconds } : mission;
|
|
443
|
+
return { mission: limited, engine_id: null, requested_engine: null, engine_fallback_reason: null };
|
|
437
444
|
}
|
|
438
445
|
// A mission owned by a team member runs on that member's roster pick. An
|
|
439
446
|
// explicit preferred engine (or a run's --engine) still wins, and with no
|
|
@@ -1722,7 +1729,18 @@ function appendMemberLog(owner, title, fields = {}, root = process.cwd()) {
|
|
|
1722
1729
|
.map(([key, value]) => `- ${key}: ${String(value).replace(/\n/g, ' ')}`),
|
|
1723
1730
|
'',
|
|
1724
1731
|
];
|
|
1725
|
-
|
|
1732
|
+
let lead = '';
|
|
1733
|
+
try {
|
|
1734
|
+
const size = fs.statSync(logPath).size;
|
|
1735
|
+
if (size > 0) {
|
|
1736
|
+
const fd = fs.openSync(logPath, 'r');
|
|
1737
|
+
const last = Buffer.alloc(1);
|
|
1738
|
+
fs.readSync(fd, last, 0, 1, size - 1);
|
|
1739
|
+
fs.closeSync(fd);
|
|
1740
|
+
if (last[0] !== 0x0a) lead = '\n';
|
|
1741
|
+
}
|
|
1742
|
+
} catch {}
|
|
1743
|
+
fs.appendFileSync(logPath, lead + rows.join('\n'), 'utf8');
|
|
1726
1744
|
return logPath;
|
|
1727
1745
|
}
|
|
1728
1746
|
|
|
@@ -3909,6 +3927,30 @@ function readTasteReviewHistory(root, limit = 4) {
|
|
|
3909
3927
|
return { accepted, revised };
|
|
3910
3928
|
}
|
|
3911
3929
|
|
|
3930
|
+
// Why the mission's own task was last sent back, or '' when it was not. Ticks
|
|
3931
|
+
// run in a worktree, so fall back to the main checkout's task projection.
|
|
3932
|
+
function readBoundTaskTurnDown(mission, root) {
|
|
3933
|
+
const bound = mission?.xp_task || mission?.selected_target_task || null;
|
|
3934
|
+
if (!bound) return '';
|
|
3935
|
+
const keys = new Set([bound.task_id, bound.id, bound.ref, bound.task_ref].filter(Boolean).map(String));
|
|
3936
|
+
if (!keys.size) return '';
|
|
3937
|
+
const roots = [root];
|
|
3938
|
+
const common = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd: root, encoding: 'utf8' });
|
|
3939
|
+
if (common.status === 0 && String(common.stdout || '').trim()) roots.push(path.dirname(String(common.stdout).trim()));
|
|
3940
|
+
for (const dir of roots) {
|
|
3941
|
+
const projection = readJson(path.join(dir, '.atris', 'state', 'tasks.projection.json'));
|
|
3942
|
+
const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
3943
|
+
const task = tasks.find((row) => row && [row.id, row.display_id, row.legacy_ref].some((key) => key && keys.has(String(key))));
|
|
3944
|
+
if (!task) continue;
|
|
3945
|
+
const metadata = task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
3946
|
+
if (metadata.approval_status && metadata.approval_status !== 'revise') return '';
|
|
3947
|
+
const events = (Array.isArray(task.events) ? task.events : []).filter((event) => event?.event_type === 'revision_requested');
|
|
3948
|
+
const note = metadata.human_revision_note || events[events.length - 1]?.payload?.note || '';
|
|
3949
|
+
return missionTasteSnippet(String(note), 600);
|
|
3950
|
+
}
|
|
3951
|
+
return '';
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3912
3954
|
function missionTasteMemorySignals(text) {
|
|
3913
3955
|
const definitions = [
|
|
3914
3956
|
['plain_language', 'the operator asks for plain English', /\b(plain|jargon|feynman|understand|simple|clarify|clarity)\b/i],
|
|
@@ -6509,9 +6551,16 @@ function writeReceipt(mission, result, root = process.cwd()) {
|
|
|
6509
6551
|
const receiptPath = path.join(paths.runsDir, `mission-${mission.id}-${safeTime}.json`);
|
|
6510
6552
|
const relativeReceiptPath = path.relative(root, receiptPath);
|
|
6511
6553
|
const finalResult = normalizeMissionReceiptResult(mission, result, relativeReceiptPath);
|
|
6554
|
+
// tree_hash covers only doctrine files; git_tree pins the code the run's
|
|
6555
|
+
// checks saw, so a later reader can tell whether the code has moved since.
|
|
6556
|
+
// With unsaved changes the checks saw more than any commit holds, so no
|
|
6557
|
+
// code tree is recorded and clean is false.
|
|
6558
|
+
const codeState = require('../lib/member-autoland').gitCodeState(root);
|
|
6512
6559
|
const receipt = {
|
|
6513
6560
|
schema: 'atris.mission_receipt.v1',
|
|
6514
6561
|
tree_hash: treeHashFor(root),
|
|
6562
|
+
clean: codeState.clean,
|
|
6563
|
+
git_tree: codeState.clean ? codeState.tree : null,
|
|
6515
6564
|
mission_id: mission.id,
|
|
6516
6565
|
objective: mission.objective,
|
|
6517
6566
|
owner: mission.owner,
|
|
@@ -8930,6 +8979,16 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8930
8979
|
checkFeedback,
|
|
8931
8980
|
]
|
|
8932
8981
|
: [];
|
|
8982
|
+
let turnDown = '';
|
|
8983
|
+
try { turnDown = readBoundTaskTurnDown(mission, mission.worktree_root || mission.worktree?.path || cwd); } catch {}
|
|
8984
|
+
const turnDownLines = turnDown
|
|
8985
|
+
? [
|
|
8986
|
+
``,
|
|
8987
|
+
`## why the last try was turned down`,
|
|
8988
|
+
`the reviewer sent this task back. fix this before anything else.`,
|
|
8989
|
+
turnDown,
|
|
8990
|
+
]
|
|
8991
|
+
: [];
|
|
8933
8992
|
const preloadedContextLines = buildPreloadedMemberContextLines(mission, cwd);
|
|
8934
8993
|
const lines = [
|
|
8935
8994
|
`# Mission Tick ${tickIndex}/${maxTicks}`,
|
|
@@ -8945,6 +9004,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8945
9004
|
`**Last tick:** ${mission.last_tick_at || 'never'}`,
|
|
8946
9005
|
...missionBudgetPromptLines(mission),
|
|
8947
9006
|
...checkFeedbackLines,
|
|
9007
|
+
...turnDownLines,
|
|
8948
9008
|
...(sharedProcess ? ['', sharedProcess] : []),
|
|
8949
9009
|
...preloadedContextLines,
|
|
8950
9010
|
``,
|
|
@@ -9181,11 +9241,20 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9181
9241
|
if (dispatchDepth.tooDeep) return Promise.resolve(tooDeepRunnerResult(dispatchDepth));
|
|
9182
9242
|
return new Promise((resolve) => {
|
|
9183
9243
|
let promptFile = null;
|
|
9244
|
+
let lastMessageFile = null;
|
|
9184
9245
|
let cmd = '';
|
|
9185
9246
|
let briefId = null;
|
|
9186
9247
|
try {
|
|
9187
9248
|
promptFile = writeRunnerPromptFile(cwd, mission.id, prompt);
|
|
9188
9249
|
cmd = buildRunnerCommand({ promptFile, model, ...(mission.roster_effort ? { effort: mission.roster_effort } : {}) });
|
|
9250
|
+
// A codex that prints JSONL events gets live step lines; its final
|
|
9251
|
+
// answer comes from the last-message file, the same text stdout held.
|
|
9252
|
+
const lastFile = promptFile.replace(/\.md$/, '.last.txt');
|
|
9253
|
+
const eventsCmd = codexJsonEventsCommand(cmd, lastFile);
|
|
9254
|
+
if (eventsCmd) {
|
|
9255
|
+
cmd = eventsCmd;
|
|
9256
|
+
lastMessageFile = lastFile;
|
|
9257
|
+
}
|
|
9189
9258
|
const engine = canonicalEngineName(mission.runner);
|
|
9190
9259
|
if (engine) {
|
|
9191
9260
|
const record = appendBriefRecord(cwd, {
|
|
@@ -9224,7 +9293,16 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9224
9293
|
}
|
|
9225
9294
|
|
|
9226
9295
|
const startedAt = Date.now();
|
|
9227
|
-
const
|
|
9296
|
+
const progressMember = mission.owner || 'mission';
|
|
9297
|
+
// A codex run opens its shell script with the run marker as a no-op, so
|
|
9298
|
+
// member stop can prove the shell leading this group is this run. Only
|
|
9299
|
+
// names member stop accepts go into the shell line.
|
|
9300
|
+
const runToken = lastMessageFile && /^[A-Za-z0-9._-]+$/.test(progressMember) ? newRunToken() : '';
|
|
9301
|
+
const markerPrefix = runToken ? `: ${runMarker(progressMember, runToken)}; ` : '';
|
|
9302
|
+
// The trailing exit keeps a shell like dash from replacing itself with
|
|
9303
|
+
// codex, which would take the marker out of the process list.
|
|
9304
|
+
const markerSuffix = runToken ? '; exit $?' : '';
|
|
9305
|
+
const guardedCommand = `${markerPrefix}PATH=${shellQuote(gitGuard.pathPrefix)}:$PATH; export PATH; ${cmd}${markerSuffix}`;
|
|
9228
9306
|
const proc = spawn('sh', ['-lc', guardedCommand], {
|
|
9229
9307
|
cwd,
|
|
9230
9308
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -9232,13 +9310,33 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9232
9310
|
env: { ...gitGuard.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
|
|
9233
9311
|
});
|
|
9234
9312
|
updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
|
|
9313
|
+
const seenItemIds = new Set();
|
|
9314
|
+
if (lastMessageFile) {
|
|
9315
|
+
const tag = runToken ? { pid: proc.pid, engine: 'codex', run: runToken } : {};
|
|
9316
|
+
appendProgress(progressMember, startedLine(mission.objective, tag), mission.id);
|
|
9317
|
+
}
|
|
9235
9318
|
let stdout = '';
|
|
9319
|
+
let eventBuf = '';
|
|
9320
|
+
let lastAgentMessage = '';
|
|
9236
9321
|
let stderr = '';
|
|
9237
9322
|
let timedOut = false;
|
|
9238
9323
|
let aborted = false;
|
|
9239
9324
|
|
|
9240
9325
|
const cleanupPrompt = () => {
|
|
9241
9326
|
try { if (promptFile) fs.unlinkSync(promptFile); } catch {}
|
|
9327
|
+
try { if (lastMessageFile) fs.unlinkSync(lastMessageFile); } catch {}
|
|
9328
|
+
};
|
|
9329
|
+
const readCodexEvent = (line) => {
|
|
9330
|
+
let ev;
|
|
9331
|
+
try { ev = JSON.parse(line); } catch { return; }
|
|
9332
|
+
const item = ev && ev.item;
|
|
9333
|
+
if (ev.type === 'item.completed' && item && item.type === 'agent_message' && typeof item.text === 'string') {
|
|
9334
|
+
lastAgentMessage = item.text;
|
|
9335
|
+
}
|
|
9336
|
+
const step = formatCodexEvent(ev, cwd);
|
|
9337
|
+
if (!step || (item && item.id && seenItemIds.has(item.id))) return;
|
|
9338
|
+
if (item && item.id) seenItemIds.add(item.id);
|
|
9339
|
+
reportProgress(progressMember, step, mission.id);
|
|
9242
9340
|
};
|
|
9243
9341
|
const kill = () => {
|
|
9244
9342
|
const killGroup = (sig) => {
|
|
@@ -9254,18 +9352,36 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9254
9352
|
else signal.addEventListener('abort', onAbort, { once: true });
|
|
9255
9353
|
}
|
|
9256
9354
|
|
|
9257
|
-
proc.stdout.on('data', (chunk) => {
|
|
9355
|
+
proc.stdout.on('data', (chunk) => {
|
|
9356
|
+
const text = chunk.toString();
|
|
9357
|
+
stdout += text;
|
|
9358
|
+
if (!lastMessageFile) return;
|
|
9359
|
+
eventBuf += text;
|
|
9360
|
+
let nl;
|
|
9361
|
+
while ((nl = eventBuf.indexOf('\n')) !== -1) {
|
|
9362
|
+
const line = eventBuf.slice(0, nl).trim();
|
|
9363
|
+
eventBuf = eventBuf.slice(nl + 1);
|
|
9364
|
+
if (line) readCodexEvent(line);
|
|
9365
|
+
}
|
|
9366
|
+
});
|
|
9258
9367
|
proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
9259
9368
|
|
|
9260
9369
|
proc.on('close', (code) => {
|
|
9261
9370
|
clearTimeout(timer);
|
|
9371
|
+
if (lastMessageFile && eventBuf.trim()) readCodexEvent(eventBuf.trim());
|
|
9372
|
+
let finalText = String(stdout || '').trim();
|
|
9373
|
+
if (lastMessageFile) {
|
|
9374
|
+
let lastMessage = '';
|
|
9375
|
+
try { lastMessage = fs.readFileSync(lastMessageFile, 'utf8'); } catch {}
|
|
9376
|
+
finalText = String(lastMessage || lastAgentMessage || '').trim();
|
|
9377
|
+
}
|
|
9262
9378
|
cleanupPrompt();
|
|
9263
9379
|
gitGuard.cleanup();
|
|
9264
9380
|
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
9265
9381
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
9266
|
-
const finalText = String(stdout || '').trim();
|
|
9267
9382
|
const errStr = String(stderr || '').slice(-2000);
|
|
9268
9383
|
const ok = code === 0 && !timedOut && !aborted;
|
|
9384
|
+
if (lastMessageFile) appendProgress(progressMember, `finished: ${ok ? 'ok' : 'failed'}`, mission.id);
|
|
9269
9385
|
const authExpired = !ok && missionRunnerAuthExpired(errStr, finalText);
|
|
9270
9386
|
resolve({
|
|
9271
9387
|
ok,
|
|
@@ -9355,8 +9471,10 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9355
9471
|
});
|
|
9356
9472
|
return;
|
|
9357
9473
|
}
|
|
9474
|
+
const runToken = newRunToken();
|
|
9475
|
+
const runOwner = mission.owner || 'mission';
|
|
9358
9476
|
const args = [
|
|
9359
|
-
'-p', prompt
|
|
9477
|
+
'-p', `${runMarker(runOwner, runToken)}\n${prompt}`,
|
|
9360
9478
|
'--output-format', 'stream-json',
|
|
9361
9479
|
'--verbose',
|
|
9362
9480
|
'--permission-mode', 'bypassPermissions',
|
|
@@ -9380,7 +9498,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9380
9498
|
updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
|
|
9381
9499
|
const progressMember = mission.owner || 'mission';
|
|
9382
9500
|
const seenToolIds = new Set();
|
|
9383
|
-
appendProgress(progressMember,
|
|
9501
|
+
appendProgress(progressMember, startedLine(mission.objective, { pid: proc.pid, engine: 'claude', run: runToken }), mission.id);
|
|
9384
9502
|
|
|
9385
9503
|
let stdoutBuf = '';
|
|
9386
9504
|
let observedSessionIds = new Set();
|
|
@@ -9532,10 +9650,13 @@ function engineVerifierResultFromRun(engineResult, engine = 'codex') {
|
|
|
9532
9650
|
}
|
|
9533
9651
|
|
|
9534
9652
|
function pickCrossEngineVerifier(mission, cwd = process.cwd(), preferredEngine = '') {
|
|
9535
|
-
|
|
9536
|
-
|
|
9653
|
+
// The reviewer must not be the builder: a builder grading itself is no review.
|
|
9654
|
+
// A named reviewer is used unless it is the builder's own engine.
|
|
9537
9655
|
const workerEngine = canonicalEngineName(mission?.runner) || 'claude';
|
|
9538
|
-
const
|
|
9656
|
+
const explicit = canonicalEngineName(preferredEngine || mission?.referee_engine || '');
|
|
9657
|
+
if (explicit && explicit !== workerEngine) return explicit;
|
|
9658
|
+
const candidates = (workerEngine === 'codex' ? ['claude', 'fable'] : ['codex', 'claude'])
|
|
9659
|
+
.filter((candidate) => candidate !== workerEngine);
|
|
9539
9660
|
try {
|
|
9540
9661
|
const engines = engineRegistryView(cwd);
|
|
9541
9662
|
for (const candidate of candidates) {
|
|
@@ -9613,6 +9734,20 @@ function cappedClaudeReceiptText(text, limit = 4000) {
|
|
|
9613
9734
|
return clean.slice(0, limit - 16).trimEnd() + '\n...[truncated]';
|
|
9614
9735
|
}
|
|
9615
9736
|
|
|
9737
|
+
// The receipt keeps a capped copy of the runner's report. When the report is
|
|
9738
|
+
// longer than that, the whole text goes to its own file beside the receipts
|
|
9739
|
+
// so nothing the member wrote is lost.
|
|
9740
|
+
function saveFullRunnerReport(mission, text, root = process.cwd()) {
|
|
9741
|
+
const clean = String(text || '').trim();
|
|
9742
|
+
if (!clean || cappedClaudeReceiptText(clean) === clean) return null;
|
|
9743
|
+
const runsDir = statePaths(root).runsDir;
|
|
9744
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
9745
|
+
const safeTime = stampIso().replace(/[:.]/g, '-');
|
|
9746
|
+
const reportPath = path.join(runsDir, `mission-${mission.id}-${safeTime}-report.md`);
|
|
9747
|
+
fs.writeFileSync(reportPath, clean + '\n', 'utf8');
|
|
9748
|
+
return path.relative(root, reportPath);
|
|
9749
|
+
}
|
|
9750
|
+
|
|
9616
9751
|
async function parseAndValidateMissionRunPhase(args) {
|
|
9617
9752
|
const asJson = wantsJson(args);
|
|
9618
9753
|
const context = {
|
|
@@ -10238,6 +10373,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10238
10373
|
brief_id: claudeResult.brief_id || null,
|
|
10239
10374
|
summary: claudeResult.summary,
|
|
10240
10375
|
receipt_text: claudeResult.receipt_text,
|
|
10376
|
+
report_path: saveFullRunnerReport(mission, claudeResult.result, cwd),
|
|
10241
10377
|
stop_reason: claudeResult.stop_reason,
|
|
10242
10378
|
api_equivalent_estimate: claudeResult.api_equivalent_estimate,
|
|
10243
10379
|
duration_total_ms: claudeResult.duration_total_ms,
|
|
@@ -10704,12 +10840,22 @@ async function verifyMissionRunTickPhase(context) {
|
|
|
10704
10840
|
|| (!currentTick.storedVerifier && !process.env.NODE_TEST_CONTEXT)
|
|
10705
10841
|
);
|
|
10706
10842
|
if (shouldRunReferee) {
|
|
10707
|
-
|
|
10843
|
+
// Pin the code before the referee looks; a referee that changes the
|
|
10844
|
+
// code or its unsaved state has not reviewed what would merge.
|
|
10845
|
+
const { gitCodeState, codeStatus } = require('../lib/member-autoland');
|
|
10846
|
+
const beforeReview = gitCodeState(cwd);
|
|
10847
|
+
const reviewed = await runEngineVerifier(currentTick.tickRuntimeMission, {
|
|
10708
10848
|
cwd,
|
|
10709
10849
|
signal: controller.signal,
|
|
10710
10850
|
tickIndex: currentTick.tickIdx,
|
|
10711
10851
|
refereeEngine: context.refereeEngine,
|
|
10712
10852
|
});
|
|
10853
|
+
const afterReview = gitCodeState(cwd);
|
|
10854
|
+
const codeMoved = beforeReview.tree !== afterReview.tree
|
|
10855
|
+
|| codeStatus(beforeReview) !== codeStatus(afterReview)
|
|
10856
|
+
|| beforeReview.hidden !== afterReview.hidden;
|
|
10857
|
+
const refereeResult = codeMoved ? { ...reviewed, passed: false, reason: 'code changed during review' } : reviewed;
|
|
10858
|
+
result.reviewed_git_tree = beforeReview.clean ? beforeReview.tree : null;
|
|
10713
10859
|
result.referee_result = refereeResult;
|
|
10714
10860
|
if (!verifierResult || !refereeResult.passed) {
|
|
10715
10861
|
verifierResult = verifierResult
|
package/commands/sync.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { ensureWikiScaffold } = require('../lib/wiki');
|
|
5
5
|
const { upsertAtrisClaudeBootBlock } = require('../lib/claude-boot-block');
|
|
6
|
+
const { ensureRunIgnore } = require('../lib/gitignore');
|
|
6
7
|
const {
|
|
7
8
|
upsertAgentVoiceCard,
|
|
8
9
|
upsertClaudeVoiceHook,
|
|
@@ -492,6 +493,10 @@ function syncAtris(options = {}) {
|
|
|
492
493
|
dryRun,
|
|
493
494
|
force,
|
|
494
495
|
});
|
|
496
|
+
const bizRunIgnore = ensureRunIgnore(process.cwd(), { dryRun });
|
|
497
|
+
if (bizRunIgnore.changed) {
|
|
498
|
+
console.log(`${dryRun ? 'Would update' : '✓ Updated'} .gitignore (atris run output ignored)`);
|
|
499
|
+
}
|
|
495
500
|
exportTeamAfterUpdate(process.cwd(), { dryRun });
|
|
496
501
|
if (!yes) {
|
|
497
502
|
console.log('\nPlan only. Re-run with --yes to write these files.');
|
|
@@ -730,6 +735,14 @@ Key behaviors:
|
|
|
730
735
|
applyClaudeUpdate(content, 'create CLAUDE.md with atris block', 'created CLAUDE.md with atris block');
|
|
731
736
|
}
|
|
732
737
|
|
|
738
|
+
// Member run output must stay out of git or member self-merge never fires
|
|
739
|
+
// on a dirty tree. Append-only; plan-only mode prints instead of writing.
|
|
740
|
+
const runIgnore = ensureRunIgnore(process.cwd(), { dryRun });
|
|
741
|
+
if (runIgnore.changed) {
|
|
742
|
+
console.log(`${dryRun ? 'Would update' : '✓ Updated'} .gitignore (atris run output ignored)`);
|
|
743
|
+
updated++;
|
|
744
|
+
}
|
|
745
|
+
|
|
733
746
|
if (dryRun) {
|
|
734
747
|
console.log(`\nDry run: ${updated} file(s) would update, ${skipped} unchanged; no changes made.`);
|
|
735
748
|
if (!yes) {
|
package/commands/task.js
CHANGED
|
@@ -253,6 +253,7 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
253
253
|
atris task archive <id> --reason "..." [--from-failed]
|
|
254
254
|
Sweep off-roadmap/duplicate work as archived (not failed);
|
|
255
255
|
--from-failed opts in to relabel a fail-closed row (never done)
|
|
256
|
+
atris task reopen <id> [--reason "..."] Send a done, failed, or archived task back to open
|
|
256
257
|
atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
|
|
257
258
|
atris task prune-test-junk [--yes] [--vacuum] [--json]
|
|
258
259
|
Count rows test runs left from temp folders; --yes backs up the db, then removes them
|
|
@@ -6243,6 +6244,27 @@ function cmdAdd(args) {
|
|
|
6243
6244
|
const taskDb = getTaskDb();
|
|
6244
6245
|
const db = taskDb.open();
|
|
6245
6246
|
const ws = taskDb.workspaceRoot();
|
|
6247
|
+
// Filing a job that is already open or claimed hands back that job instead
|
|
6248
|
+
// of making a copy. Work already handed in for review can be filed again.
|
|
6249
|
+
const existing = taskDb.findUnfinishedTaskByTitle(db, { workspaceRoot: ws, title });
|
|
6250
|
+
if (existing) {
|
|
6251
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
6252
|
+
const task = compactTaskFromProjection(projection, existing.id);
|
|
6253
|
+
if (wantsJson(args)) {
|
|
6254
|
+
printJson({
|
|
6255
|
+
ok: true,
|
|
6256
|
+
action: 'reused',
|
|
6257
|
+
task_id: existing.id,
|
|
6258
|
+
inserted: false,
|
|
6259
|
+
operator_title_warning: null,
|
|
6260
|
+
projection_path: outPath,
|
|
6261
|
+
task,
|
|
6262
|
+
});
|
|
6263
|
+
return;
|
|
6264
|
+
}
|
|
6265
|
+
console.log(`${taskRef(task)}\t${existing.title}\t(already on the list, reused)`);
|
|
6266
|
+
return;
|
|
6267
|
+
}
|
|
6246
6268
|
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
|
|
6247
6269
|
// Generation throttle, the named root cause is generation > human-review rate. An AGENT cannot keep
|
|
6248
6270
|
// minting tasks while a wall of certified-but-unaccepted work waits; that treadmill is what accept-group
|
|
@@ -9738,6 +9760,45 @@ function cmdArchive(args) {
|
|
|
9738
9760
|
}
|
|
9739
9761
|
}
|
|
9740
9762
|
|
|
9763
|
+
// Send a done, failed, or archived task back to open so it can be claimed
|
|
9764
|
+
// again. Review sign-offs are cleared; the old status and reason are kept.
|
|
9765
|
+
function cmdReopen(args) {
|
|
9766
|
+
const pos = positional(args);
|
|
9767
|
+
const id = pos[0];
|
|
9768
|
+
if (!id) failTask('atris task reopen', 'missing_id', 'task id required');
|
|
9769
|
+
const reason = textFlag(args, ['--reason']) || 'reopened';
|
|
9770
|
+
const taskDb = getTaskDb();
|
|
9771
|
+
const db = taskDb.open();
|
|
9772
|
+
const taskId = requireTaskId(taskDb, db, id, 'atris task reopen');
|
|
9773
|
+
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
9774
|
+
const result = taskDb.reopenTask(db, { id: taskId, actor, reason });
|
|
9775
|
+
if (!result.reopened) {
|
|
9776
|
+
const detail = `reopen failed: ${taskId} ${result.reason}`;
|
|
9777
|
+
if (wantsJson(args)) {
|
|
9778
|
+
printJson({ ok: false, command: 'atris task reopen', reason: result.reason, task_id: taskId, detail });
|
|
9779
|
+
process.exit(1);
|
|
9780
|
+
}
|
|
9781
|
+
console.error(detail);
|
|
9782
|
+
process.exit(1);
|
|
9783
|
+
}
|
|
9784
|
+
const reopenedFrom = result.row && result.row.metadata && result.row.metadata.reopened_from || null;
|
|
9785
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
9786
|
+
const task = compactTaskFromProjection(projection, taskId);
|
|
9787
|
+
if (wantsJson(args)) {
|
|
9788
|
+
printJson({
|
|
9789
|
+
ok: true,
|
|
9790
|
+
action: 'reopened',
|
|
9791
|
+
task_id: taskId,
|
|
9792
|
+
reopened_from: reopenedFrom,
|
|
9793
|
+
reason,
|
|
9794
|
+
projection_path: outPath,
|
|
9795
|
+
task,
|
|
9796
|
+
});
|
|
9797
|
+
return;
|
|
9798
|
+
}
|
|
9799
|
+
console.log(`reopened ${taskRef(task)}${reopenedFrom ? ` (was ${reopenedFrom})` : ''}: ${reason}`);
|
|
9800
|
+
}
|
|
9801
|
+
|
|
9741
9802
|
function taskCompletionTime(row) {
|
|
9742
9803
|
const doneAt = Number(row && row.done_at);
|
|
9743
9804
|
if (Number.isFinite(doneAt) && doneAt > 0) return doneAt;
|
|
@@ -13475,6 +13536,7 @@ async function runTaskCommand(args) {
|
|
|
13475
13536
|
case 'finish': return cmdFinish(rest);
|
|
13476
13537
|
case 'fail': return cmdDone([...rest, '--failed']);
|
|
13477
13538
|
case 'archive': return cmdArchive(rest);
|
|
13539
|
+
case 'reopen': return cmdReopen(rest);
|
|
13478
13540
|
case 'clear-done': return cmdClearDone(rest);
|
|
13479
13541
|
case 'prune-test-junk': return cmdPruneTestJunk(rest);
|
|
13480
13542
|
case 'keep': return cmdKeep(rest);
|
|
@@ -13521,7 +13583,7 @@ const MUTATING_TASK_COMMANDS = new Set([
|
|
|
13521
13583
|
'continue-work', 'continue', 'chat', 'note', 'say', 'retitle', 'tag', 'tags', 'step',
|
|
13522
13584
|
'ready', 'result', 'accept', 'landing', 'land-review', 'auto-accept-certified',
|
|
13523
13585
|
'auto-accept', 'sweep', 'audit', 'certify-verified', 'accept-group', 'revise',
|
|
13524
|
-
'done', 'finish', 'fail', 'archive', 'clear-done', 'reap-mission-blockers', 'reap-blockers', 'relabel-archived', 'review', 'import',
|
|
13586
|
+
'done', 'finish', 'fail', 'archive', 'reopen', 'clear-done', 'reap-mission-blockers', 'reap-blockers', 'relabel-archived', 'review', 'import',
|
|
13525
13587
|
'setup', 'review-lane-act', 'review-act', 'act-review', 'review-lane-loop',
|
|
13526
13588
|
'review-loop', 'loop-review', 'review-lane-run', 'review-run', 'run-review',
|
|
13527
13589
|
]);
|
package/commands/worktree.js
CHANGED
|
@@ -159,6 +159,26 @@ function branchName(member, task, now = new Date()) {
|
|
|
159
159
|
return `codex/${slugify(member, 'member', 24)}-${slugify(task, 'task', 36)}-${stamp(now)}`;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// The newest earlier run of this same member+job, if its commits never reached
|
|
163
|
+
// the base, so a retry continues that work instead of redoing it. Only the
|
|
164
|
+
// newest run counts: once it landed or did nothing, anything older is stale.
|
|
165
|
+
function lastRunBranch(root, owner, task, base, skip) {
|
|
166
|
+
const prefix = `codex/${slugify(owner, 'member', 24)}-${slugify(task, 'task', 36)}-`;
|
|
167
|
+
const listed = runGit(['for-each-ref', '--format=%(refname:short)', `refs/heads/${prefix}*`], { cwd: root, check: false });
|
|
168
|
+
if (listed.status !== 0) return null;
|
|
169
|
+
// Only exact runs of this job (stamp plus optional same-second number), so
|
|
170
|
+
// "fix-login" never matches "fix-login-page".
|
|
171
|
+
const runs = listed.stdout.split('\n').map((name) => name.trim())
|
|
172
|
+
.map((name) => ({ name, run: name.slice(prefix.length).match(/^(\d{8}-\d{6})(?:-(\d+))?$/) }))
|
|
173
|
+
.filter(({ name, run }) => run && name !== skip)
|
|
174
|
+
.sort((a, b) => b.run[1].localeCompare(a.run[1]) || Number(b.run[2] || 1) - Number(a.run[2] || 1));
|
|
175
|
+
if (!runs.length) return null;
|
|
176
|
+
const newest = runs[0].name;
|
|
177
|
+
const cherry = runGit(['cherry', base, newest], { cwd: root, check: false });
|
|
178
|
+
if (cherry.status !== 0) return null;
|
|
179
|
+
return cherry.stdout.split('\n').some((line) => line.startsWith('+')) ? newest : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
162
182
|
function defaultWorktreePath(root, member, task, now = new Date()) {
|
|
163
183
|
const name = `${slugify(member, 'member', 24)}-${slugify(task, 'task', 36)}-${stamp(now)}`;
|
|
164
184
|
return path.join(path.dirname(root), '.agent-worktrees', path.basename(root), name);
|
|
@@ -529,8 +549,18 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
529
549
|
reapedBeforeStart = cleaned.removed;
|
|
530
550
|
cachePrunedBeforeStart = cleaned.cachePruned;
|
|
531
551
|
} catch {}
|
|
532
|
-
|
|
533
|
-
|
|
552
|
+
let branch = branchOverride || branchName(owner, task, now);
|
|
553
|
+
let target = path.resolve(pathOverride || defaultWorktreePath(root, owner, task, now));
|
|
554
|
+
// Two runs of the same job in the same second get the same name; number
|
|
555
|
+
// the later one instead of failing it.
|
|
556
|
+
if (!pathOverride && !branchOverride) {
|
|
557
|
+
const baseBranch = branch;
|
|
558
|
+
const baseTarget = target;
|
|
559
|
+
for (let n = 2; fs.existsSync(target) && n < 100; n += 1) {
|
|
560
|
+
branch = `${baseBranch}-${n}`;
|
|
561
|
+
target = `${baseTarget}-${n}`;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
534
564
|
// Agent/member worktrees always cut from the mainline: honoring the
|
|
535
565
|
// launcher checkout's upstream let a dispatch inherit another session's
|
|
536
566
|
// in-progress feature branch (2026-07-09: an auth fix arrived carrying 13
|
|
@@ -540,9 +570,15 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
540
570
|
if (fs.existsSync(target)) throw new Error(`worktree path already exists: ${target}`);
|
|
541
571
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
542
572
|
const staleBase = refreshRemoteRef(root, checkoutBase);
|
|
573
|
+
// A retry of the same job starts from the last run's unlanded commits; it
|
|
574
|
+
// still ships to the mainline.
|
|
575
|
+
const resumedFrom = (!pathOverride && !branchOverride)
|
|
576
|
+
? lastRunBranch(root, owner, task, checkoutBase, branch)
|
|
577
|
+
: null;
|
|
578
|
+
const startPoint = resumedFrom || checkoutBase;
|
|
543
579
|
const ref = `refs/heads/${branch}`;
|
|
544
580
|
const existed = runGit(['show-ref', '--verify', '--quiet', ref], { cwd: root, check: false }).status !== 1;
|
|
545
|
-
const baseCommit = runGit(['rev-parse', '--verify', `${
|
|
581
|
+
const baseCommit = runGit(['rev-parse', '--verify', `${startPoint}^{commit}`], { cwd: root, check: false });
|
|
546
582
|
const startCommit = baseCommit.status === 0 ? baseCommit.stdout.trim() : '';
|
|
547
583
|
// With no local copy the start fails on its own; only a real cut gets the warning.
|
|
548
584
|
if (staleBase && startCommit) {
|
|
@@ -562,7 +598,7 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
562
598
|
if (!fs.existsSync(target)) runGit(['update-ref', '-d', ref, startCommit], { cwd: root });
|
|
563
599
|
};
|
|
564
600
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
565
|
-
const added = runGit(['worktree', 'add', '-b', branch, target,
|
|
601
|
+
const added = runGit(['worktree', 'add', '-b', branch, target, startPoint], { cwd: root, check: false });
|
|
566
602
|
if (added.status === 0) break;
|
|
567
603
|
// Git can create the branch before failing to write its tracking config.
|
|
568
604
|
// Drop only that untouched ref, never an existing branch or a checkout.
|
|
@@ -587,6 +623,7 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
587
623
|
branch,
|
|
588
624
|
base: shipBase,
|
|
589
625
|
checkout_base: checkoutBase,
|
|
626
|
+
resumed_from: resumedFrom,
|
|
590
627
|
workspace_root: root,
|
|
591
628
|
created_at: now.toISOString(),
|
|
592
629
|
}, null, 2) + '\n',
|
|
@@ -596,7 +633,7 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
596
633
|
discardFailedStart();
|
|
597
634
|
throw error;
|
|
598
635
|
}
|
|
599
|
-
return { path: target, branch, base: shipBase, checkoutBase, owner, reapedBeforeStart, cachePrunedBeforeStart };
|
|
636
|
+
return { path: target, branch, base: shipBase, checkoutBase, resumedFrom, owner, reapedBeforeStart, cachePrunedBeforeStart };
|
|
600
637
|
}
|
|
601
638
|
|
|
602
639
|
function startWorktree(args) {
|
package/lib/engine-ask.js
CHANGED
|
@@ -415,6 +415,7 @@ function runAskProcess(invocation, {
|
|
|
415
415
|
killProcess = process.kill.bind(process),
|
|
416
416
|
signal = null,
|
|
417
417
|
onOutputChunk = null,
|
|
418
|
+
env = null,
|
|
418
419
|
} = {}) {
|
|
419
420
|
return new Promise((resolve) => {
|
|
420
421
|
const startedAt = Date.now();
|
|
@@ -502,7 +503,7 @@ function runAskProcess(invocation, {
|
|
|
502
503
|
try {
|
|
503
504
|
child = spawnProcess(invocation.bin, invocation.args, {
|
|
504
505
|
cwd,
|
|
505
|
-
env: buildAskSpawnEnv(),
|
|
506
|
+
env: buildAskSpawnEnv(env || invocation.env || process.env),
|
|
506
507
|
detached: process.platform !== 'win32',
|
|
507
508
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
508
509
|
});
|
package/lib/engine-registry.js
CHANGED
|
@@ -182,11 +182,12 @@ const ENGINE_SEED_META = Object.freeze({
|
|
|
182
182
|
// bake the effort into the model.
|
|
183
183
|
models: Object.freeze([
|
|
184
184
|
'gemini-3.8-flash',
|
|
185
|
+
// Matches `agy models` on agy 1.3.4, verified 2026-10-13.
|
|
186
|
+
'gemini-4-argon-high',
|
|
187
|
+
'gemini-4-argon-medium',
|
|
185
188
|
'gemini-3.8-flash-high',
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
'claude-sonnet-4-6',
|
|
189
|
-
'claude-opus-4-6-thinking',
|
|
189
|
+
'claude-opus-5-5-high',
|
|
190
|
+
'claude-sonnet-5-5-high',
|
|
190
191
|
'gpt-oss-120b-medium',
|
|
191
192
|
]),
|
|
192
193
|
duty: 'errands',
|