atris 3.74.11 → 3.74.12
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/atris/team/executor/MEMBER.md +1 -0
- package/commands/member.js +67 -10
- package/commands/mission.js +3 -1
- package/lib/member-autoland.js +8 -1
- package/lib/member-save-work.js +54 -0
- package/package.json +1 -1
|
@@ -124,6 +124,7 @@ If new evidence changes the task, stop and add a task note instead of forcing co
|
|
|
124
124
|
7. **Anti-slop aware**: Read `atris/policies/ANTISLOP.md` before writing. No sparkles, no filler, no purple prose.
|
|
125
125
|
8. **Stay in scope**: Only touch files listed in the task. If you need to change something outside scope, stop and flag it. That's a new task.
|
|
126
126
|
9. **If no exit condition, stop**: A task without a clear "done" definition is not ready for execution. Send it back to navigator.
|
|
127
|
+
10. **Make exactly what was asked**: Match the existing output shape, names, and style. Add no fields, flags, or features the job did not ask for; name them as a next step instead.
|
|
127
128
|
|
|
128
129
|
---
|
|
129
130
|
|
package/commands/member.js
CHANGED
|
@@ -976,9 +976,22 @@ function inferMemberRunVerifier(missionText, args = [], cwd = process.cwd()) {
|
|
|
976
976
|
if (testFileMatch && fs.existsSync(path.join(cwd, testFileMatch[1]))) {
|
|
977
977
|
return `node --test ${testFileMatch[1]}`;
|
|
978
978
|
}
|
|
979
|
+
// A job that names the project's own test command ("keep npm test green")
|
|
980
|
+
// chose that check; it is not a silent broad-suite default.
|
|
981
|
+
const npmMatch = text.match(/\bnpm\s+(run\s+)?test(?![\w:-]|\.\w)/i);
|
|
982
|
+
if (npmMatch && projectHasTestScript(cwd)) return npmMatch[1] ? 'npm run test' : 'npm test';
|
|
979
983
|
return '';
|
|
980
984
|
}
|
|
981
985
|
|
|
986
|
+
function projectHasTestScript(cwd) {
|
|
987
|
+
try {
|
|
988
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
989
|
+
return Boolean(pkg && pkg.scripts && pkg.scripts.test);
|
|
990
|
+
} catch {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
982
995
|
function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cwd()) {
|
|
983
996
|
const resolvedVerifier = inferMemberRunVerifier(missionText, args, cwd);
|
|
984
997
|
const startArgs = [
|
|
@@ -1023,7 +1036,10 @@ function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cw
|
|
|
1023
1036
|
// Default to an isolated worktree only when there is a git repo to cut it
|
|
1024
1037
|
// from. A plain (non-git) workspace would fail every mission start with
|
|
1025
1038
|
// "fatal: not a git repository", degrade to the shared checkout instead.
|
|
1026
|
-
|
|
1039
|
+
// A job that says "do not commit" wants the change in the person's own
|
|
1040
|
+
// files; a side checkout would hide uncommitted work where they never see it.
|
|
1041
|
+
const forbidsCommit = !hasFlag(args, '--worktree') && require('../lib/member-save-work').jobForbidsCommit(missionText);
|
|
1042
|
+
if (!forbidsCommit && !hasFlag(args, '--shared-checkout') && !hasFlag(args, '--no-worktree') && insideGitRepo(cwd)) {
|
|
1027
1043
|
startArgs.push('--worktree');
|
|
1028
1044
|
}
|
|
1029
1045
|
startArgs.push('--json');
|
|
@@ -1434,6 +1450,17 @@ function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
|
|
|
1434
1450
|
result.ask = `${name} is waiting on you: ${payload.human_asks[0]}`;
|
|
1435
1451
|
result.next_command = payload.next_command || 'atris decide';
|
|
1436
1452
|
}
|
|
1453
|
+
// Passed but never committed: save the loose work on the member's branch so
|
|
1454
|
+
// the branch holds it. Only in the member's own side checkout, never the main one.
|
|
1455
|
+
const worktreePath = finalMission.worktree?.path || mission.worktree?.path || '';
|
|
1456
|
+
if (ok && started && worktreePath && finalMission.verifier_result?.passed === true) {
|
|
1457
|
+
result.saved_work = require('../lib/member-save-work').saveLeftoverMemberWork({
|
|
1458
|
+
worktreePath, member: name, objective: finalMission.objective || mission.objective,
|
|
1459
|
+
});
|
|
1460
|
+
if (result.saved_work.saved && !asJson && !quiet) {
|
|
1461
|
+
console.log(`saved ${result.saved_work.files.length} file(s) ${name} left unsaved onto its branch`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1437
1464
|
// A finished ignited run whose check passed hands its branch to GitHub
|
|
1438
1465
|
// auto-merge, only past the local gates; any held gate leaves the branch.
|
|
1439
1466
|
if (ignite && ok && started && !reviewAsk && finalMission.verifier_result?.passed === true) {
|
|
@@ -9946,13 +9973,24 @@ async function memberChat(name, ...rest) {
|
|
|
9946
9973
|
|
|
9947
9974
|
const IDEA_ARCHETYPE_RULES = [
|
|
9948
9975
|
{
|
|
9949
|
-
|
|
9950
|
-
|
|
9976
|
+
// A job that opens by asking where or which, or forbids source edits, is
|
|
9977
|
+
// a read-and-answer job even when it mentions a bug.
|
|
9978
|
+
slugs: ['navigator'],
|
|
9979
|
+
// Wins wherever it matches: the question shape decides the whole job.
|
|
9980
|
+
leading: true,
|
|
9981
|
+
pattern: /^(?![\s\S]*(?<!not\s|n't\s)\b(fix|repair|implement|update|change|edit|add|rename|modify)\b)\s*(find|locate|which|count|trace|identify)\b|\bdo\s+not\s+(edit|change|modify)\s+(any\s+)?source\b/i,
|
|
9982
|
+
reason: 'reads the code and answers where and which questions',
|
|
9983
|
+
},
|
|
9984
|
+
{
|
|
9985
|
+
// The shipped `improver` proposes team and workflow changes; it is not a
|
|
9986
|
+
// code fixer, so fix jobs go to auto-improver or fall back to the builder.
|
|
9987
|
+
slugs: ['auto-improver'],
|
|
9988
|
+
pattern: /\b(repeat|recurring|broken|flaky|errors?|regressions?|pain|fix(es|ed|ing)?|bugs?|fail(s|ing|ed|ure)?|self[- ]improv|crash(es|ed|ing)?)\b/i,
|
|
9951
9989
|
reason: 'handles recurring errors, regressions, and test fixes',
|
|
9952
9990
|
},
|
|
9953
9991
|
{
|
|
9954
9992
|
slugs: ['validator', 'orb'],
|
|
9955
|
-
pattern: /\b(review|audit|validate|verify|check\s+diff|quality|slop|security)\b/i,
|
|
9993
|
+
pattern: /\b(code\s+review|review|audit|validate|verify|check\s+diff|quality|slop|security)\b/i,
|
|
9956
9994
|
reason: 'reviews changes and blocks weak work before merge',
|
|
9957
9995
|
},
|
|
9958
9996
|
{
|
|
@@ -9988,8 +10026,10 @@ const ARCHETYPE_FOR_JOB = { build: 'executor', review: 'validator', search: 'res
|
|
|
9988
10026
|
const EXPERIMENT_STATUSES = new Set(['experimental', 'one-off']);
|
|
9989
10027
|
|
|
9990
10028
|
function ideaRuleFor(slug, record, text) {
|
|
9991
|
-
const
|
|
9992
|
-
|
|
10029
|
+
const ownRules = IDEA_ARCHETYPE_RULES.filter((rule) => rule.slugs.includes(slug));
|
|
10030
|
+
const matched = ownRules.find((rule) => rule.pattern.test(text));
|
|
10031
|
+
if (matched) return { rule: matched, own: true };
|
|
10032
|
+
const own = ownRules[0];
|
|
9993
10033
|
const { autoJobForMember } = require('../lib/member-engine');
|
|
9994
10034
|
const archetype = ARCHETYPE_FOR_JOB[autoJobForMember(slug, record.role)];
|
|
9995
10035
|
const rule = IDEA_ARCHETYPE_RULES.find((r) => r.slugs.includes(archetype)) || null;
|
|
@@ -10000,7 +10040,15 @@ function ideaRuleFor(slug, record, text) {
|
|
|
10000
10040
|
// specific jobs to the general build job, so "where is the code" is a
|
|
10001
10041
|
// navigator job even though "code" is also a build word.
|
|
10002
10042
|
function ideaJobRule(text) {
|
|
10003
|
-
|
|
10043
|
+
// The job word that comes first names the job: "review the bug fix" is a
|
|
10044
|
+
// review, "fix the review page" is a fix. Ties keep list order.
|
|
10045
|
+
let best = null;
|
|
10046
|
+
for (const rule of IDEA_ARCHETYPE_RULES) {
|
|
10047
|
+
const match = rule.pattern.exec(text);
|
|
10048
|
+
const index = match ? (rule.leading ? -1 : match.index) : null;
|
|
10049
|
+
if (match && (!best || index < best.index)) best = { rule, index };
|
|
10050
|
+
}
|
|
10051
|
+
return best ? best.rule : null;
|
|
10004
10052
|
}
|
|
10005
10053
|
|
|
10006
10054
|
// 3 = named in the idea, 2 = the job is the member's own, 1 = the member
|
|
@@ -10059,7 +10107,12 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
10059
10107
|
.toLowerCase()
|
|
10060
10108
|
.split(/[^a-z0-9]+/)
|
|
10061
10109
|
.filter((w) => w.length >= 4);
|
|
10062
|
-
|
|
10110
|
+
let jobRule = ideaJobRule(text);
|
|
10111
|
+
// A job nobody on this team owns goes to the builder rule, not to whoever
|
|
10112
|
+
// shares the most words with it.
|
|
10113
|
+
if (jobRule && !members.some((m) => ideaRuleFor(memberSlugFromRecord(m), m, text).rule === jobRule)) {
|
|
10114
|
+
jobRule = IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes('executor')) || jobRule;
|
|
10115
|
+
}
|
|
10063
10116
|
|
|
10064
10117
|
let best = null;
|
|
10065
10118
|
for (const record of members) {
|
|
@@ -10073,6 +10126,9 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
10073
10126
|
|
|
10074
10127
|
const memberRule = ideaRuleFor(slug, record, text);
|
|
10075
10128
|
const fit = ideaJobFit(named, memberRule, jobRule);
|
|
10129
|
+
// A reviewer never patches, so it only takes review jobs unless named.
|
|
10130
|
+
const reviewRule = IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes('validator'));
|
|
10131
|
+
const effectiveTier = !named && memberRule.rule === reviewRule && jobRule !== reviewRule ? Math.max(tier, 1) : tier;
|
|
10076
10132
|
if (fit > 0 && memberRule.rule) {
|
|
10077
10133
|
score += 30;
|
|
10078
10134
|
matchedReason = memberRule.rule.reason;
|
|
@@ -10105,7 +10161,7 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
10105
10161
|
role: record.role || 'Team Member',
|
|
10106
10162
|
reason: matchedReason,
|
|
10107
10163
|
score,
|
|
10108
|
-
tier,
|
|
10164
|
+
tier: effectiveTier,
|
|
10109
10165
|
fit,
|
|
10110
10166
|
};
|
|
10111
10167
|
if (outranksDispatch(candidate, best)) best = candidate;
|
|
@@ -10438,7 +10494,7 @@ function resolveMemberEngineSeat(memberSlug, root = process.cwd()) {
|
|
|
10438
10494
|
|
|
10439
10495
|
function buildMemberSubagentShell(owner, role, engineId, objective, root = process.cwd()) {
|
|
10440
10496
|
const cleanObjective = compactSentence(objective || `Advance ${owner} roadmap`, 160);
|
|
10441
|
-
const verify = inferMemberRunVerifier(
|
|
10497
|
+
const verify = inferMemberRunVerifier(cleanObjective, [], root);
|
|
10442
10498
|
return {
|
|
10443
10499
|
role: `${owner} (${role || 'Team Member'})`,
|
|
10444
10500
|
member: owner,
|
|
@@ -12068,6 +12124,7 @@ module.exports = {
|
|
|
12068
12124
|
wakeBootLines,
|
|
12069
12125
|
buildMemberRunStartArgs,
|
|
12070
12126
|
inferMemberRunVerifier,
|
|
12127
|
+
buildMemberSubagentShell,
|
|
12071
12128
|
pickMemberForIdea,
|
|
12072
12129
|
appendMemberGoalLog,
|
|
12073
12130
|
appendProjectLog,
|
package/commands/mission.js
CHANGED
|
@@ -2620,7 +2620,8 @@ const MISSION_BARE_COMMAND_WORDS = new Set(['go', 'keep', 'going', 'do', 'it', '
|
|
|
2620
2620
|
function missionObjectiveIsJunk(value) {
|
|
2621
2621
|
const objective = String(value || '').trim();
|
|
2622
2622
|
if (!objective || objective.startsWith('-')) return true;
|
|
2623
|
-
|
|
2623
|
+
// Line breaks and tabs are normal in a pasted bug report; other control bytes are junk.
|
|
2624
|
+
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\ufffd]/u.test(objective)) return true;
|
|
2624
2625
|
if (objective.replace(/\s/g, '').length < 8) return true;
|
|
2625
2626
|
const words = objective.toLowerCase().match(/[a-z]+/g) || [];
|
|
2626
2627
|
return words.length > 0
|
|
@@ -9304,6 +9305,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
9304
9305
|
``,
|
|
9305
9306
|
`## Landing rule`,
|
|
9306
9307
|
`Safe-lane work (internal code, tests, docs, and proofs) must be committed and pushed or landed WITHOUT asking a human. Protected lanes (external sends, payments, credentials, auth/CSP/iframe/sandbox surfaces, and deletes of user data) are never self-landed: file a decision card instead.`,
|
|
9308
|
+
`The objective's own limits win over this rule: if it says do not commit, touch only certain files, or leave something alone, do exactly that.`,
|
|
9307
9309
|
`After landing a ladder-sourced slice, append a judge card to atris/status/for-you.md in this SHIPPED format:`,
|
|
9308
9310
|
missionShippedCard(),
|
|
9309
9311
|
``,
|
package/lib/member-autoland.js
CHANGED
|
@@ -446,7 +446,14 @@ function landLocally({ headSha, branch, cwd, pinnedTree }) {
|
|
|
446
446
|
function autolandMemberRun({ mission, cwd, summary = '', workerEngine = '', loadTask = defaultLoadTask, closeTaskIds = [] } = {}) {
|
|
447
447
|
if (!mission || typeof mission !== 'object') return held('mission', 'the mission could not be read');
|
|
448
448
|
if (mission.verifier_result?.passed !== true) return held('check', 'the check did not pass');
|
|
449
|
-
if (!policyOn(cwd))
|
|
449
|
+
if (!policyOn(cwd)) {
|
|
450
|
+
// Say where the work is and the one command that brings it in; "self-landing
|
|
451
|
+
// is off" alone left people thinking the fix was already in their files.
|
|
452
|
+
const branch = String(mission.worktree?.branch || '').trim();
|
|
453
|
+
const gate = held('policy', 'self-landing is not switched on in the main checkout');
|
|
454
|
+
if (!branch || branch === mainCheckoutBranch(cwd)) return gate;
|
|
455
|
+
return { ...gate, branch, line: `your files are not changed yet: the work is saved on branch ${branch}. bring it in: git merge ${branch}` };
|
|
456
|
+
}
|
|
450
457
|
|
|
451
458
|
const missionLane = protectedLane({ ...mission, title: mission.objective });
|
|
452
459
|
if (missionLane) return held('protected', `this mission sits in a protected area (${missionLane})`);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
// A member that passed its check but never committed leaves its work loose in
|
|
8
|
+
// a side checkout that cleanup later deletes (member lab 2026-10-24: navigator
|
|
9
|
+
// and validator wrote the right files, said done, committed nothing). Commit
|
|
10
|
+
// what is left so the branch holds the work. Atris's own state stays out.
|
|
11
|
+
function git(args, cwd) {
|
|
12
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
13
|
+
return { ok: result.status === 0, stdout: String(result.stdout || '').trim() };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function jobForbidsCommit(text) {
|
|
17
|
+
return /\b(do\s+not|don't|never|without|no)\s+(?:(?:create|make|add|any|new|a|an|more)\s+){0,3}commit/i.test(String(text || ''));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Files that look like secrets never ride an automatic commit.
|
|
21
|
+
const SECRET_PATH = /(^|\/)(\.(aws|ssh|gnupg|docker|kube)\/|\.env(\.[^/]*)?$|\.npmrc$|\.netrc$|credentials?$|[^/]*credentials?[^/]*\.(json|ya?ml|txt)$|secrets?\.(json|ya?ml|txt|env)$|[^/]*\.(pem|key|p12|pfx)$|id_(rsa|ed25519|ecdsa)[^/]*$|service[-_]account[^/]*\.json$)/i;
|
|
22
|
+
|
|
23
|
+
function saveLeftoverMemberWork({ worktreePath, member = 'member', objective = '' } = {}) {
|
|
24
|
+
if (!worktreePath) return { saved: false, reason: 'no_worktree' };
|
|
25
|
+
// The job's own words win: "do not commit" means leave the changes loose.
|
|
26
|
+
if (jobForbidsCommit(objective)) return { saved: false, reason: 'job_forbids_commit' };
|
|
27
|
+
if (!git(['rev-parse', '--is-inside-work-tree'], worktreePath).ok) return { saved: false, reason: 'not_git' };
|
|
28
|
+
// Only a linked side checkout: its git dir differs from the shared one. The
|
|
29
|
+
// person's main checkout is never committed into.
|
|
30
|
+
// Plain --git-dir/--git-common-dir work on old git too; resolve both against
|
|
31
|
+
// the checkout, since either may come back relative.
|
|
32
|
+
const gitDirOut = git(['rev-parse', '--git-dir'], worktreePath).stdout;
|
|
33
|
+
const commonDirOut = git(['rev-parse', '--git-common-dir'], worktreePath).stdout;
|
|
34
|
+
if (!gitDirOut || !commonDirOut) return { saved: false, reason: 'not_side_checkout' };
|
|
35
|
+
const real = (dir) => { try { return fs.realpathSync(path.resolve(worktreePath, dir)); } catch { return ''; } };
|
|
36
|
+
const gitDir = real(gitDirOut);
|
|
37
|
+
const commonDir = real(commonDirOut);
|
|
38
|
+
if (!gitDir || !commonDir || gitDir === commonDir) return { saved: false, reason: 'not_side_checkout' };
|
|
39
|
+
const add = git(['add', '-A', '--', '.', ':(exclude).atris', ':(exclude).agent-worktrees'], worktreePath);
|
|
40
|
+
if (!add.ok) return { saved: false, reason: 'add_failed' };
|
|
41
|
+
git(['reset', '-q', '--', '.atris', '.agent-worktrees'], worktreePath);
|
|
42
|
+
const stagedAll = git(['diff', '--cached', '--name-only'], worktreePath).stdout.split('\n').filter(Boolean);
|
|
43
|
+
const skipped = stagedAll.filter((file) => SECRET_PATH.test(file));
|
|
44
|
+
if (skipped.length) git(['reset', '-q', '--', ...skipped], worktreePath);
|
|
45
|
+
const staged = stagedAll.filter((file) => !SECRET_PATH.test(file));
|
|
46
|
+
if (!staged.length) return { saved: false, reason: 'nothing_left', ...(skipped.length ? { skipped } : {}) };
|
|
47
|
+
const title = String(objective || '').split('\n').map((line) => line.trim()).find(Boolean) || 'finished work';
|
|
48
|
+
const message = `${member}: ${title.slice(0, 72)}\n\nSaved by atris after the check passed; the member left these changes uncommitted.`;
|
|
49
|
+
const commit = git(['commit', '-q', '-m', message], worktreePath);
|
|
50
|
+
if (!commit.ok) return { saved: false, reason: 'commit_failed', files: staged };
|
|
51
|
+
return { saved: true, files: staged, ...(skipped.length ? { skipped } : {}), sha: git(['rev-parse', 'HEAD'], worktreePath).stdout };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { jobForbidsCommit, saveLeftoverMemberWork };
|
package/package.json
CHANGED