atris 3.74.12 → 3.74.13
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/commands/member.js +20 -4
- package/commands/mission.js +71 -2
- package/lib/zen-apply.js +12 -4
- package/lib/zen.js +1 -0
- package/package.json +1 -1
package/commands/member.js
CHANGED
|
@@ -785,6 +785,7 @@ const MEMBER_RUN_START_BOOLEAN_FLAGS = [
|
|
|
785
785
|
'--json',
|
|
786
786
|
'--no-verify',
|
|
787
787
|
'--referee',
|
|
788
|
+
'--referee-if-check-edited',
|
|
788
789
|
'--no-referee',
|
|
789
790
|
'--worktree',
|
|
790
791
|
'--shared-checkout',
|
|
@@ -1064,9 +1065,21 @@ function startMemberRunMission(name, missionText, args = []) {
|
|
|
1064
1065
|
}
|
|
1065
1066
|
|
|
1066
1067
|
// An ignited run asks a second engine to review the work by default, so every
|
|
1067
|
-
// run ends with a verdict.
|
|
1068
|
-
|
|
1069
|
-
|
|
1068
|
+
// run ends with a verdict. A real test command is already that verdict: the
|
|
1069
|
+
// second engine only reran the same commands and cost about a minute a run
|
|
1070
|
+
// (member lab 2026-10-24), so it stays for runs whose check is not a test
|
|
1071
|
+
// runner (whitespace, syntax, or file-exists checks). --no-referee skips it; an explicit --referee is kept.
|
|
1072
|
+
function memberRunHasRealCheck(verifier) {
|
|
1073
|
+
const command = String(verifier || '').trim();
|
|
1074
|
+
// A plain test runner only: chaining (|| true, ; , &&, pipes, subshells) or a
|
|
1075
|
+
// pass-with-no-tests switch can make any run "pass", so those keep the review.
|
|
1076
|
+
if (/\|\||&&|[;|`\r\n]|\$\(|pass-?with-?no-?tests|--no-tests|--exit-zero/i.test(command)) return false;
|
|
1077
|
+
return /^((npm|pnpm|yarn)\s+(run\s+)?test\b|node\s+--test\b|npx\s+(jest|vitest|mocha)\b|pytest\b|python3?\s+-m\s+pytest\b|go\s+test\b|cargo\s+test\b)/i.test(command);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function memberRunWantsReferee(runArgs, ignite, verifier = '') {
|
|
1081
|
+
return Boolean(ignite) && !hasFlag(runArgs, '--referee') && !hasFlag(runArgs, '--no-referee')
|
|
1082
|
+
&& !memberRunHasRealCheck(verifier);
|
|
1070
1083
|
}
|
|
1071
1084
|
|
|
1072
1085
|
function memberRunCli(args, cwd, { live = false, missionId = '' } = {}) {
|
|
@@ -1393,7 +1406,10 @@ function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
|
|
|
1393
1406
|
['--mission', '--mission-id', '--minutes', '--hours', '--industry', '--domain', '--value', '--outcome', '--truth', '--proof', '--base', '--yes-lock', '--yes-lock-parent'],
|
|
1394
1407
|
['--worktree', '--shared-checkout', '--no-worktree', '--choose-work', '--auto', '--ignite', '--wait', '--sync']);
|
|
1395
1408
|
if (ignite && !hasFlag(runArgs, '--complete-on-pass')) runArgs.push('--complete-on-pass');
|
|
1396
|
-
if (memberRunWantsReferee(runArgs, ignite)) runArgs.push('--referee');
|
|
1409
|
+
if (memberRunWantsReferee(runArgs, ignite, mission.verifier)) runArgs.push('--referee');
|
|
1410
|
+
// Skipped because a real test is the check: review anyway if the worker
|
|
1411
|
+
// edits what that test command runs.
|
|
1412
|
+
else if (ignite && memberRunHasRealCheck(mission.verifier) && !hasFlag(runArgs, '--no-referee')) runArgs.push('--referee-if-check-edited');
|
|
1397
1413
|
const budgetSeconds = memberRunBudgetSeconds(args);
|
|
1398
1414
|
if (!readFlag(runArgs, '--max-ticks', '')) {
|
|
1399
1415
|
runArgs.push('--max-ticks', budgetSeconds ? String(Math.max(4, Math.ceil(budgetSeconds / 300))) : '1');
|
package/commands/mission.js
CHANGED
|
@@ -788,6 +788,7 @@ const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
|
788
788
|
'--no-claude',
|
|
789
789
|
'--no-verify',
|
|
790
790
|
'--referee',
|
|
791
|
+
'--referee-if-check-edited',
|
|
791
792
|
'--no-referee',
|
|
792
793
|
'--complete-on-pass',
|
|
793
794
|
'--no-drain',
|
|
@@ -6950,6 +6951,39 @@ function missionProtectedTags(mission, root = process.cwd()) {
|
|
|
6950
6951
|
return tags;
|
|
6951
6952
|
}
|
|
6952
6953
|
|
|
6954
|
+
// True when the tick changed what its check command runs: package.json (which
|
|
6955
|
+
// defines npm test) or a file the command names. A passing test the worker
|
|
6956
|
+
// rewrote is not evidence on its own.
|
|
6957
|
+
function tickTouchedCheckDefinition(verifier, before, root = process.cwd()) {
|
|
6958
|
+
const run = (args) => spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
6959
|
+
const head = String(before?.head || '').trim();
|
|
6960
|
+
const changed = [
|
|
6961
|
+
// No starting commit to compare against: every tracked file counts.
|
|
6962
|
+
...String(run(head ? ['diff', '--name-only', head] : ['ls-files']).stdout || '').split('\n'),
|
|
6963
|
+
...String(run(['diff', '--name-only', 'HEAD']).stdout || '').split('\n'),
|
|
6964
|
+
...String(run(['ls-files', '--others', '--exclude-standard']).stdout || '').split('\n'),
|
|
6965
|
+
].map((file) => file.trim()).filter(Boolean);
|
|
6966
|
+
// npm test runs whatever package.json says, so its paths and folders count too.
|
|
6967
|
+
let checkText = String(verifier || '');
|
|
6968
|
+
if (/^(npm|pnpm|yarn)\s/i.test(checkText.trim())) {
|
|
6969
|
+
try { checkText += ` ${JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))?.scripts?.test || ''}`; } catch {}
|
|
6970
|
+
}
|
|
6971
|
+
// Runners that find tests by name (node --test, jest, pytest, go test) never
|
|
6972
|
+
// list them, so any changed file shaped like a test counts as well.
|
|
6973
|
+
const testShaped = /(^|\/)(tests?|__tests__|specs?|checks?)\/|\.(test|spec)\.[cm]?[jt]sx?$|_test\.(go|py)$|(^|\/)test_[^/]*\.py$|(^|\/)conftest\.py$/i;
|
|
6974
|
+
return changed.some((file) => path.basename(file) === 'package.json'
|
|
6975
|
+
|| testShaped.test(file)
|
|
6976
|
+
|| checkText.includes(file)
|
|
6977
|
+
|| (path.dirname(file) !== '.' && checkText.includes(`${path.dirname(file)}/`)));
|
|
6978
|
+
}
|
|
6979
|
+
|
|
6980
|
+
// The worker's `learned:` line from its receipt, so the lesson reaches the
|
|
6981
|
+
// member log (and the next worker's preloaded memory) without a hand edit.
|
|
6982
|
+
function receiptLearnedLine(text) {
|
|
6983
|
+
const match = String(text || '').match(/^[\s*-]*learned:\s*(.+)$/im);
|
|
6984
|
+
return match ? match[1].trim().slice(0, 240) : undefined;
|
|
6985
|
+
}
|
|
6986
|
+
|
|
6953
6987
|
function inspectMissionTickProtectedDiff(mission, before, root = process.cwd()) {
|
|
6954
6988
|
const after = gitWorktreeSnapshot(root);
|
|
6955
6989
|
if (before?.reason === 'not-git-worktree' && after?.reason === 'not-git-worktree') {
|
|
@@ -9153,6 +9187,31 @@ function readBoundedContextFile(filePath, maxChars = 1600) {
|
|
|
9153
9187
|
}
|
|
9154
9188
|
}
|
|
9155
9189
|
|
|
9190
|
+
// Code members never call the account-wide tool servers (mail, calendar,
|
|
9191
|
+
// docs), yet loading them cost about 5s per worker start in the member lab.
|
|
9192
|
+
// They keep the project's own .mcp.json; ATRIS_MEMBER_KEEP_MCP=1 keeps all.
|
|
9193
|
+
const CODE_MEMBERS = new Set(['executor', 'builder', 'codex-executor', 'auto-improver', 'navigator', 'validator', 'architect']);
|
|
9194
|
+
|
|
9195
|
+
function claudeRunnerMcpArgs(mission, cwd = process.cwd(), env = process.env) {
|
|
9196
|
+
if (env.ATRIS_MEMBER_KEEP_MCP === '1') return [];
|
|
9197
|
+
if (!CODE_MEMBERS.has(String(mission?.owner || '').trim().toLowerCase())) return [];
|
|
9198
|
+
const args = ['--strict-mcp-config'];
|
|
9199
|
+
const projectConfig = path.join(cwd, '.mcp.json');
|
|
9200
|
+
if (fs.existsSync(projectConfig)) args.push('--mcp-config', projectConfig);
|
|
9201
|
+
// Servers added for this folder with Claude's local scope live in
|
|
9202
|
+
// ~/.claude.json under the folder's path; keep those too.
|
|
9203
|
+
try {
|
|
9204
|
+
const home = env.HOME || os.homedir();
|
|
9205
|
+
const projects = JSON.parse(fs.readFileSync(path.join(home, '.claude.json'), 'utf8'))?.projects || {};
|
|
9206
|
+
// A side checkout's servers are usually registered under the main checkout.
|
|
9207
|
+
const common = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd, encoding: 'utf8' });
|
|
9208
|
+
const mainRoot = common.status === 0 ? path.dirname(path.resolve(cwd, String(common.stdout || '').trim())) : '';
|
|
9209
|
+
const local = { ...(mainRoot ? projects[mainRoot]?.mcpServers : null), ...projects[path.resolve(cwd)]?.mcpServers };
|
|
9210
|
+
if (Object.keys(local).length) args.push('--mcp-config', JSON.stringify({ mcpServers: local }));
|
|
9211
|
+
} catch {}
|
|
9212
|
+
return args;
|
|
9213
|
+
}
|
|
9214
|
+
|
|
9156
9215
|
function buildPreloadedMemberContextLines(mission, root = process.cwd()) {
|
|
9157
9216
|
const owner = String(mission?.owner || '').trim();
|
|
9158
9217
|
const baseRoot = mission?.worktree_root || mission?.worktree?.path || root;
|
|
@@ -9282,7 +9341,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
9282
9341
|
``,
|
|
9283
9342
|
`## Your task`,
|
|
9284
9343
|
`Do ONE increment of work toward the stop condition. ONE. No more.`,
|
|
9285
|
-
`- ${memberIdentityLine(mission.owner)}
|
|
9344
|
+
`- ${memberIdentityLine(mission.owner)} Your member files are preloaded above; open atris/team/${mission.owner}/MEMBER.md only if that copy is marked truncated. Work in that identity, inside its scope and stop rules. Atris writes your member log from your receipt, so end the receipt with one \`learned: <one line>\` line instead of editing the log.`,
|
|
9286
9345
|
`- FIRST: inspect current mission/task state before acting. Read the relevant files, run \`atris mission status ${mission.id}\`, \`git status\`, or \`atris task list\` as needed so you know what's already done.`,
|
|
9287
9346
|
`- Pick the smallest concrete action that moves the mission forward.`,
|
|
9288
9347
|
`- Before acting, state your single next move in one sentence.`,
|
|
@@ -9290,7 +9349,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
9290
9349
|
`- The tick is not done until the verify step passes. If a frozen verifier exists, the harness runs it, but that does not replace the real-surface proof below. If no frozen verifier exists, finish by actually running the surface you changed on this computer and paste the real command output into your receipt.`,
|
|
9291
9350
|
`- After that one verify step, STOP. Never start a second slice in one tick.`,
|
|
9292
9351
|
`- Write the tick summary in operator language: what changed and what it buys or costs in plain words, with no flags, task ids, or code identifiers.`,
|
|
9293
|
-
`- Propose the next needle-mover: include a \`next_push: <one concrete feature or optimization to build next>\` line in your receipt
|
|
9352
|
+
`- Propose the next needle-mover: include a \`next_push: <one concrete feature or optimization to build next>\` line in your receipt so the team compounds exponentially.`,
|
|
9294
9353
|
``,
|
|
9295
9354
|
`## No claimable work? Work the backstop ladder`,
|
|
9296
9355
|
`If the mission and task state offer no claimable work, inspect these rungs in order. Pick the FIRST rung with real fuel and do ONE bounded slice:`,
|
|
@@ -9686,6 +9745,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9686
9745
|
result: finalText,
|
|
9687
9746
|
summary: usefulClaudeReceiptSummary(finalText || errStr, ok ? 'no-text' : 'error'),
|
|
9688
9747
|
receipt_text: cappedClaudeReceiptText(finalText || errStr),
|
|
9748
|
+
learned: receiptLearnedLine(finalText),
|
|
9689
9749
|
duration_total_ms: Date.now() - startedAt,
|
|
9690
9750
|
num_turns: null,
|
|
9691
9751
|
stop_reason: null,
|
|
@@ -9775,6 +9835,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9775
9835
|
];
|
|
9776
9836
|
if (model) args.push('--model', model);
|
|
9777
9837
|
if (mission.roster_effort) args.push('--effort', mission.roster_effort);
|
|
9838
|
+
args.push(...claudeRunnerMcpArgs(mission, cwd));
|
|
9778
9839
|
if (sessionMode === 'set') args.push('--session-id', sessionId);
|
|
9779
9840
|
else if (sessionMode === 'resume') args.push('--resume', sessionId);
|
|
9780
9841
|
|
|
@@ -9881,6 +9942,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9881
9942
|
result: finalText,
|
|
9882
9943
|
summary: usefulClaudeReceiptSummary(finalText, ok ? 'no-text' : 'error'),
|
|
9883
9944
|
receipt_text: cappedClaudeReceiptText(finalText),
|
|
9945
|
+
learned: receiptLearnedLine(finalText),
|
|
9884
9946
|
api_equivalent_estimate: costEstimate,
|
|
9885
9947
|
duration_api_ms: durationApiMs,
|
|
9886
9948
|
duration_total_ms: Date.now() - startedAt,
|
|
@@ -10124,6 +10186,7 @@ async function parseAndValidateMissionRunPhase(args) {
|
|
|
10124
10186
|
const skipClaude = hasFlag(args, '--no-claude');
|
|
10125
10187
|
const verifyEach = !hasFlag(args, '--no-verify');
|
|
10126
10188
|
const refereeEach = hasFlag(args, '--referee') && !hasFlag(args, '--no-referee');
|
|
10189
|
+
const refereeIfCheckEdited = hasFlag(args, '--referee-if-check-edited') && !hasFlag(args, '--no-referee');
|
|
10127
10190
|
const refereeEngine = readFlag(args, '--referee-engine', '');
|
|
10128
10191
|
const completeOnPass = hasFlag(args, '--complete-on-pass');
|
|
10129
10192
|
const skipDrain = hasFlag(args, '--no-drain');
|
|
@@ -10161,6 +10224,7 @@ async function parseAndValidateMissionRunPhase(args) {
|
|
|
10161
10224
|
skipClaude,
|
|
10162
10225
|
verifyEach,
|
|
10163
10226
|
refereeEach,
|
|
10227
|
+
refereeIfCheckEdited,
|
|
10164
10228
|
refereeEngine,
|
|
10165
10229
|
completeOnPass,
|
|
10166
10230
|
skipDrain,
|
|
@@ -10995,6 +11059,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10995
11059
|
verifier: verifierResult ? (verifierResult.passed ? 'passed' : 'failed') : 'not_run',
|
|
10996
11060
|
summary: tickSummaryText ? tickSummaryText.slice(0, 240) : undefined,
|
|
10997
11061
|
next_push: result.next_push ? String(result.next_push).slice(0, 180) : undefined,
|
|
11062
|
+
learned: result?.claude?.learned || receiptLearnedLine(result?.claude?.result || result?.claude?.receipt_text),
|
|
10998
11063
|
feedback: verifierResult && !verifierResult.passed ? String(mission.last_check_feedback || '').slice(0, 240) : undefined,
|
|
10999
11064
|
receipt: receiptPath,
|
|
11000
11065
|
}, cwd);
|
|
@@ -11192,6 +11257,7 @@ async function verifyMissionRunTickPhase(context) {
|
|
|
11192
11257
|
&& (
|
|
11193
11258
|
!currentTick.frozen.verifier
|
|
11194
11259
|
|| context.refereeEach
|
|
11260
|
+
|| (context.refereeIfCheckEdited && tickTouchedCheckDefinition(currentTick.frozen.verifier, currentTick.tickWorktreeBefore, cwd))
|
|
11195
11261
|
|| (!currentTick.storedVerifier && !process.env.NODE_TEST_CONTEXT)
|
|
11196
11262
|
);
|
|
11197
11263
|
if (shouldRunReferee) {
|
|
@@ -12860,6 +12926,9 @@ function missionCommand(args) {
|
|
|
12860
12926
|
}
|
|
12861
12927
|
|
|
12862
12928
|
module.exports = {
|
|
12929
|
+
receiptLearnedLine,
|
|
12930
|
+
tickTouchedCheckDefinition,
|
|
12931
|
+
claudeRunnerMcpArgs,
|
|
12863
12932
|
crystallizeMemberSkillLesson,
|
|
12864
12933
|
listSiblingWorkspaceMissionHints,
|
|
12865
12934
|
missionProtectedLaneHold,
|
package/lib/zen-apply.js
CHANGED
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
sideCopyLines,
|
|
32
32
|
scheduledCopies,
|
|
33
33
|
runningCopies,
|
|
34
|
+
shortPlace,
|
|
34
35
|
untrackedFiles,
|
|
35
36
|
ABANDONED_DAYS,
|
|
36
37
|
abandonedDaysFromEnv,
|
|
@@ -81,6 +82,13 @@ function identityEnv(root) {
|
|
|
81
82
|
return env;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
// Where zen put your copy of a clashing note, said from the repo root so the
|
|
86
|
+
// line fits the pane, or from home when it sits outside the repo.
|
|
87
|
+
function savedPlace(root, file) {
|
|
88
|
+
const rel = path.relative(root, file);
|
|
89
|
+
return rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? rel : shortPlace(file);
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
function archiveStashes(root, tag) {
|
|
85
93
|
const shas = (gitOut(['stash', 'list', '--format=%H'], root) || '').split('\n').filter(Boolean);
|
|
86
94
|
if (!shas.length) return null;
|
|
@@ -444,7 +452,7 @@ function moveKeepingNotes(root, args, target, tag, from = 'HEAD') {
|
|
|
444
452
|
for (const p of clashed) {
|
|
445
453
|
const mine = path.join(dir, 'mine', p);
|
|
446
454
|
if (isJournal(p) && joinJournal(path.join(root, p), mine)) notes.push(`joined your ${p} onto the new one`);
|
|
447
|
-
else if (!(fromCommit && lineRefsOnly(root, fromCommit, p, mine))) notes.push(`kept the new ${p}; your copy is in ${mine}`);
|
|
455
|
+
else if (!(fromCommit && lineRefsOnly(root, fromCommit, p, mine))) notes.push(`kept the new ${p}; your copy is in ${savedPlace(root, mine)}`);
|
|
448
456
|
}
|
|
449
457
|
for (const p of missed) {
|
|
450
458
|
const mine = path.join(dir, 'mine', p);
|
|
@@ -452,9 +460,9 @@ function moveKeepingNotes(root, args, target, tag, from = 'HEAD') {
|
|
|
452
460
|
if (to && fromCommit && followMove(root, fromCommit, p, to, mine)) {
|
|
453
461
|
notes.push(`${p} moved to ${to}, and your edits went with it`);
|
|
454
462
|
} else if (to) {
|
|
455
|
-
notes.push(`${p} moved to ${to} and your edits clash there; your copy is in ${fs.existsSync(mine) ? mine : patchFile}`);
|
|
463
|
+
notes.push(`${p} moved to ${to} and your edits clash there; your copy is in ${savedPlace(root, fs.existsSync(mine) ? mine : patchFile)}`);
|
|
456
464
|
} else {
|
|
457
|
-
notes.push(`${p} no longer fits the new changes; your copy is in ${fs.existsSync(mine) ? mine : patchFile}`);
|
|
465
|
+
notes.push(`${p} no longer fits the new changes; your copy is in ${savedPlace(root, fs.existsSync(mine) ? mine : patchFile)}`);
|
|
458
466
|
}
|
|
459
467
|
}
|
|
460
468
|
}
|
|
@@ -464,7 +472,7 @@ function moveKeepingNotes(root, args, target, tag, from = 'HEAD') {
|
|
|
464
472
|
// A copy that matches the incoming file loses nothing, so say nothing.
|
|
465
473
|
if (sameBytes(saved, path.join(root, p))) fs.rmSync(saved, { force: true });
|
|
466
474
|
else if (isJournal(p) && joinJournal(path.join(root, p), saved)) notes.push(`joined your ${p} onto the new one`);
|
|
467
|
-
else notes.push(`kept the new ${p}; your copy is in ${saved}`);
|
|
475
|
+
else notes.push(`kept the new ${p}; your copy is in ${savedPlace(root, saved)}`);
|
|
468
476
|
} else {
|
|
469
477
|
fs.mkdirSync(path.dirname(path.join(root, p)), { recursive: true });
|
|
470
478
|
fs.copyFileSync(saved, path.join(root, p));
|
package/lib/zen.js
CHANGED
package/package.json
CHANGED