atris 3.65.0 → 3.67.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 +27 -0
- package/atris/skills/engines/SKILL.md +2 -2
- package/bin/atris.js +41 -3
- package/commands/agent-client.js +266 -0
- package/commands/auth.js +225 -21
- package/commands/autoland.js +62 -0
- package/commands/brain.js +4 -0
- package/commands/engine.js +23 -1
- package/commands/link.js +367 -0
- package/commands/loops.js +4 -1
- package/commands/member.js +8 -1
- package/commands/orb.js +1 -1
- package/commands/xp.js +2 -0
- package/commands/youtube.js +1 -1
- package/lib/auto-accept-certified.js +89 -9
- package/lib/cleanup-summary.js +129 -0
- package/lib/engine-ask.js +3 -2
- package/lib/engine-registry.js +203 -15
- package/lib/idle-review-sweep.js +130 -0
- package/lib/known-commands.js +2 -2
- package/lib/land-green.js +18 -4
- package/lib/member-result.js +303 -0
- package/lib/roster-models.js +1 -1
- package/lib/runner-command.js +4 -4
- package/lib/task-list-keeper.js +25 -4
- package/lib/trust-tiers.js +131 -14
- package/lib/verify-worktree-rewrite.js +115 -0
- package/lib/voice-gate.js +2 -0
- package/package.json +1 -1
- package/scripts/det/ytnotes +1 -1
- package/utils/agent-client.js +363 -0
- package/utils/api.js +20 -2
- package/utils/auth.js +88 -3
package/commands/link.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris link "<thing a>" "<thing b>" [--context "..."] [--team] [--json]
|
|
4
|
+
//
|
|
5
|
+
// Asks what two things could become together. The answer is one short line
|
|
6
|
+
// naming the connection, a plain why, a bounded first thing to make, and the
|
|
7
|
+
// team member who should make it. The question goes to the roster's engine
|
|
8
|
+
// for the job (a `link` job when the roster has one, else the `build` lead),
|
|
9
|
+
// through the same read-only runner `atris engine ask` uses. Nothing is
|
|
10
|
+
// written except the run record line every ask leaves.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
MAX_ASK_PROMPT_BYTES,
|
|
18
|
+
buildReadOnlyEngineInvocation,
|
|
19
|
+
runAskProcess,
|
|
20
|
+
runEngineAskJobs,
|
|
21
|
+
} = require('../lib/engine-ask');
|
|
22
|
+
const { isMemberParked } = require('../lib/member-park');
|
|
23
|
+
|
|
24
|
+
const LINK_TIMEOUT_MS = 180 * 1000;
|
|
25
|
+
const TEAM_FRESH_DAYS = 5;
|
|
26
|
+
const TEAM_NOTE_CHARS = 280;
|
|
27
|
+
const MAX_LINE_WORDS = 12;
|
|
28
|
+
const MEMBER_NAME_RE = /^[a-zA-Z0-9._-]+$/;
|
|
29
|
+
|
|
30
|
+
const SYSTEM_PROMPT = [
|
|
31
|
+
'You look at two things a person cares about and say what they could become together.',
|
|
32
|
+
'',
|
|
33
|
+
'Rules:',
|
|
34
|
+
'- Use only facts stated in the two things, the context, and the team notes below. Do not read files, search, or run anything. Do not invent numbers, names, customers, or results.',
|
|
35
|
+
'- Plain words, the way a person talks. No em dashes. No hype or sales words; say the specific thing.',
|
|
36
|
+
`- line: one sentence of at most ${MAX_LINE_WORDS} words naming the connection.`,
|
|
37
|
+
'- why: two or three short sentences. Each one rests on something in the inputs.',
|
|
38
|
+
'- experiment.label: a small first thing to make, in a few words.',
|
|
39
|
+
'- experiment.scope: one sentence saying what it will do and what it will not do.',
|
|
40
|
+
'- by: one slug from the team list, the member best placed to make it, or null when none fits.',
|
|
41
|
+
'- hunch: true when the connection is a guess rather than grounded in both things; false when both things support it.',
|
|
42
|
+
'- If nothing specific stands out, answer {"line": null, "reason": "<one plain sentence>"} instead.',
|
|
43
|
+
'',
|
|
44
|
+
'Answer with exactly one JSON object and nothing else:',
|
|
45
|
+
'{"line": "...", "why": "...", "experiment": {"label": "...", "scope": "..."}, "by": "slug-or-null", "hunch": false}',
|
|
46
|
+
].join('\n');
|
|
47
|
+
|
|
48
|
+
function usage() {
|
|
49
|
+
return [
|
|
50
|
+
'usage:',
|
|
51
|
+
' atris link "<thing a>" "<thing b>" [--context "<text>"] [--team] [--json]',
|
|
52
|
+
'',
|
|
53
|
+
'asks the roster\'s engine what these two things could become together.',
|
|
54
|
+
'',
|
|
55
|
+
'options:',
|
|
56
|
+
' --context <text> extra facts the answer may use',
|
|
57
|
+
` --team read each member's now.md (fresh within ${TEAM_FRESH_DAYS} days, not parked) to pick who makes it`,
|
|
58
|
+
' --json print the answer as json',
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseLinkArgs(args = []) {
|
|
63
|
+
const things = [];
|
|
64
|
+
let context = '';
|
|
65
|
+
let team = false;
|
|
66
|
+
let json = false;
|
|
67
|
+
let help = false;
|
|
68
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
69
|
+
const arg = String(args[index]);
|
|
70
|
+
if (arg === '--help' || arg === '-h') { help = true; continue; }
|
|
71
|
+
if (arg === '--json') { json = true; continue; }
|
|
72
|
+
if (arg === '--team') { team = true; continue; }
|
|
73
|
+
if (arg === '--context') {
|
|
74
|
+
if (index + 1 >= args.length) throw new Error('--context needs text');
|
|
75
|
+
context = String(args[index + 1]);
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (arg.startsWith('--context=')) { context = arg.slice('--context='.length); continue; }
|
|
80
|
+
if (arg.startsWith('--')) throw new Error(`unknown option ${arg}`);
|
|
81
|
+
things.push(arg.trim());
|
|
82
|
+
}
|
|
83
|
+
if (help) return { help: true, json };
|
|
84
|
+
if (things.length !== 2 || !things[0] || !things[1]) throw new Error(usage());
|
|
85
|
+
return { help: false, things, context: context.trim(), team, json };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The roster's worker for this question: a `link` job when the owner made
|
|
89
|
+
// one, else whoever leads `build`.
|
|
90
|
+
function pickLinkWorker(report) {
|
|
91
|
+
const jobs = Array.isArray(report && report.jobs) ? report.jobs : [];
|
|
92
|
+
const row = jobs.find((entry) => entry && entry.job === 'link' && entry.engine)
|
|
93
|
+
|| jobs.find((entry) => entry && entry.job === 'build' && entry.engine);
|
|
94
|
+
if (!row) return null;
|
|
95
|
+
return {
|
|
96
|
+
job: row.job,
|
|
97
|
+
engine: String(row.engine),
|
|
98
|
+
model: row.model ? String(row.model) : '',
|
|
99
|
+
effort: row.effort ? String(row.effort) : '',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function rosterMemberNames(report, root) {
|
|
104
|
+
const rows = Array.isArray(report && report.team) ? report.team : [];
|
|
105
|
+
const names = [];
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
const name = String((row && row.member) || '').trim();
|
|
108
|
+
if (!name || !MEMBER_NAME_RE.test(name) || names.includes(name)) continue;
|
|
109
|
+
if (isMemberParked(root, name)) continue;
|
|
110
|
+
names.push(name);
|
|
111
|
+
}
|
|
112
|
+
return names;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function oneLine(text) {
|
|
116
|
+
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A member's now.md, trimmed to the first lines that say something: no
|
|
120
|
+
// heading marks, no "Updated:" stamp. Skipped when older than five days.
|
|
121
|
+
function teamNote(root, name, now = Date.now()) {
|
|
122
|
+
const file = path.join(root, 'atris', 'team', name, 'now.md');
|
|
123
|
+
let stat;
|
|
124
|
+
let text;
|
|
125
|
+
try {
|
|
126
|
+
stat = fs.statSync(file);
|
|
127
|
+
text = fs.readFileSync(file, 'utf8');
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (now - stat.mtimeMs > TEAM_FRESH_DAYS * 86400000) return null;
|
|
132
|
+
const lines = text.split(/\r?\n/)
|
|
133
|
+
.map((line) => line.replace(/^#+\s*/, '').trim())
|
|
134
|
+
.filter((line) => line && line.toLowerCase() !== 'now' && !/^updated:/i.test(line));
|
|
135
|
+
const note = oneLine(lines.join(' '));
|
|
136
|
+
if (!note) return null;
|
|
137
|
+
return note.length > TEAM_NOTE_CHARS ? `${note.slice(0, TEAM_NOTE_CHARS - 3)}...` : note;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildLinkPrompt({ things, context, members, notes }) {
|
|
141
|
+
const parts = [
|
|
142
|
+
SYSTEM_PROMPT,
|
|
143
|
+
'',
|
|
144
|
+
`Thing A: ${oneLine(things[0])}`,
|
|
145
|
+
`Thing B: ${oneLine(things[1])}`,
|
|
146
|
+
];
|
|
147
|
+
if (context) parts.push(`Context: ${oneLine(context)}`);
|
|
148
|
+
parts.push('');
|
|
149
|
+
if (notes && notes.length) {
|
|
150
|
+
parts.push('Team (slug: what they are on now):');
|
|
151
|
+
for (const entry of notes) parts.push(`- ${entry.name}: ${entry.note}`);
|
|
152
|
+
} else {
|
|
153
|
+
parts.push(`Team slugs: ${members.length ? members.join(', ') : '(none)'}`);
|
|
154
|
+
}
|
|
155
|
+
return parts.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Notes go in until the prompt would pass the ask limit; whoever does not
|
|
159
|
+
// fit is still a valid slug, named on the last line.
|
|
160
|
+
function fitTeamNotes(base, notes, members, maxBytes = MAX_ASK_PROMPT_BYTES - 1024) {
|
|
161
|
+
const kept = [];
|
|
162
|
+
for (const entry of notes) {
|
|
163
|
+
const next = buildLinkPrompt({ ...base, members, notes: [...kept, entry] });
|
|
164
|
+
if (Buffer.byteLength(next) > maxBytes) break;
|
|
165
|
+
kept.push(entry);
|
|
166
|
+
}
|
|
167
|
+
const rest = members.filter((name) => !kept.some((entry) => entry.name === name));
|
|
168
|
+
let prompt = buildLinkPrompt({ ...base, members, notes: kept });
|
|
169
|
+
if (kept.length && rest.length) prompt += `\nOther team slugs: ${rest.join(', ')}`;
|
|
170
|
+
return prompt;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The same read-only invocation `engine ask` builds, run from the temp
|
|
174
|
+
// folder so the workspace's own notes and memory do not leak into an answer
|
|
175
|
+
// that may use only its inputs. Claude-family engines also lose their read
|
|
176
|
+
// tools for the same reason.
|
|
177
|
+
function linkInvocation(job, timeoutMs) {
|
|
178
|
+
const invocation = buildReadOnlyEngineInvocation(job.engine, job.prompt, job.model, { timeoutMs, effort: job.effort || '' });
|
|
179
|
+
const args = [...invocation.args];
|
|
180
|
+
const tools = args.indexOf('--tools');
|
|
181
|
+
if (tools >= 0 && tools + 1 < args.length) args[tools + 1] = '';
|
|
182
|
+
return { ...invocation, args, cwd: os.tmpdir() };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function executeLinkJob(job, { timeoutMs, signal } = {}) {
|
|
186
|
+
const invocation = linkInvocation(job, timeoutMs);
|
|
187
|
+
return runAskProcess(invocation, { cwd: invocation.cwd, timeoutMs, signal });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The engine's text to one object: fences stripped, first "{" to last "}".
|
|
191
|
+
function extractJsonObject(text) {
|
|
192
|
+
const raw = String(text || '').replace(/```(?:json)?/gi, '').trim();
|
|
193
|
+
const start = raw.indexOf('{');
|
|
194
|
+
const end = raw.lastIndexOf('}');
|
|
195
|
+
if (start < 0 || end <= start) return null;
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(raw.slice(start, end + 1));
|
|
198
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Em and en dashes become commas, so a rule the model missed never reaches
|
|
205
|
+
// the board.
|
|
206
|
+
function plain(text) {
|
|
207
|
+
return oneLine(String(text == null ? '' : text)
|
|
208
|
+
.replace(/\s*[\u2014\u2013]\s*/g, ', ')
|
|
209
|
+
.replace(/,\s*,/g, ','));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function wordCount(text) {
|
|
213
|
+
return plain(text).split(' ').filter(Boolean).length;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Shape the answer into the contract. Returns { ok, value } or
|
|
217
|
+
// { ok: false, error }.
|
|
218
|
+
function normalizeLinkAnswer(parsed, members) {
|
|
219
|
+
if (!parsed) return { ok: false, error: 'the engine did not answer with a json object' };
|
|
220
|
+
if (parsed.line === null || parsed.line === undefined || !plain(parsed.line)) {
|
|
221
|
+
const reason = plain(parsed.reason) || 'nothing specific stood out between these two';
|
|
222
|
+
return { ok: true, value: { line: null, reason } };
|
|
223
|
+
}
|
|
224
|
+
const line = plain(parsed.line);
|
|
225
|
+
if (wordCount(line) > MAX_LINE_WORDS) {
|
|
226
|
+
return { ok: false, error: `the line has ${wordCount(line)} words, more than ${MAX_LINE_WORDS}` };
|
|
227
|
+
}
|
|
228
|
+
const experiment = parsed.experiment && typeof parsed.experiment === 'object' ? parsed.experiment : {};
|
|
229
|
+
const label = plain(experiment.label);
|
|
230
|
+
const scope = plain(experiment.scope);
|
|
231
|
+
const why = plain(parsed.why);
|
|
232
|
+
if (!why || !label || !scope) return { ok: false, error: 'the answer is missing why, experiment.label, or experiment.scope' };
|
|
233
|
+
const by = String(parsed.by == null ? '' : parsed.by).trim();
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
value: {
|
|
237
|
+
line,
|
|
238
|
+
why,
|
|
239
|
+
experiment: { label, scope },
|
|
240
|
+
by: by && members.includes(by) ? by : null,
|
|
241
|
+
hunch: parsed.hunch === true || String(parsed.hunch).toLowerCase() === 'true',
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function compactFailure(answer) {
|
|
247
|
+
if (!answer) return 'no answer';
|
|
248
|
+
if (answer.reason === 'timeout') return `timed out after ${Math.round((answer.duration_ms || 0) / 1000)}s`;
|
|
249
|
+
const detail = String(answer.stderr || '').trim().split(/\r?\n/).find(Boolean);
|
|
250
|
+
return detail || String(answer.reason || 'failed').replace(/_/g, ' ');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// One run-record line, job "ask", task "link", so `atris engine roster
|
|
254
|
+
// --runs` shows how long links take. Best effort.
|
|
255
|
+
function recordLinkRun(root, worker, answer, startedAt) {
|
|
256
|
+
try {
|
|
257
|
+
const { appendRosterRun, parseRunUsage } = require('../lib/roster-runs');
|
|
258
|
+
let outcome = 'landed';
|
|
259
|
+
if (answer && (answer.timed_out || answer.reason === 'timeout')) outcome = 'stalled';
|
|
260
|
+
else if (!answer || !answer.ok) outcome = 'failed';
|
|
261
|
+
appendRosterRun(root, {
|
|
262
|
+
at: startedAt,
|
|
263
|
+
job: 'ask',
|
|
264
|
+
engine: worker.engine,
|
|
265
|
+
model: worker.model,
|
|
266
|
+
...(worker.effort ? { effort: worker.effort } : {}),
|
|
267
|
+
max_seconds: Math.round(LINK_TIMEOUT_MS / 1000),
|
|
268
|
+
seconds: Math.max(0, (Number(answer && answer.duration_ms) || 0) / 1000),
|
|
269
|
+
outcome,
|
|
270
|
+
...(outcome === 'landed' ? {} : { detail: compactFailure(answer) }),
|
|
271
|
+
task: 'link',
|
|
272
|
+
source: 'link',
|
|
273
|
+
...parseRunUsage({ stdout: answer && answer.stdout, stderr: answer && answer.stderr }),
|
|
274
|
+
});
|
|
275
|
+
} catch { /* best effort */ }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function printHuman(value, ran) {
|
|
279
|
+
if (value.line === null) {
|
|
280
|
+
console.log(`\nnothing specific: ${value.reason}\n`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
console.log(`\n${value.line}${value.hunch ? ' (hunch)' : ''}`);
|
|
284
|
+
console.log(`\n ${value.why}`);
|
|
285
|
+
console.log(`\n make: ${value.experiment.label}`);
|
|
286
|
+
console.log(` ${value.experiment.scope}`);
|
|
287
|
+
console.log(` by: ${value.by || 'nobody on the team fits yet'}`);
|
|
288
|
+
console.log(`\n ${ran.engine}${ran.model ? ` (${ran.model})` : ''}, ${ran.seconds}s\n`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function linkCommand(args = [], deps = {}) {
|
|
292
|
+
const root = deps.root || process.cwd();
|
|
293
|
+
let parsed;
|
|
294
|
+
try {
|
|
295
|
+
parsed = parseLinkArgs(args);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error(`link: ${error.message}`);
|
|
298
|
+
return 2;
|
|
299
|
+
}
|
|
300
|
+
if (parsed.help) {
|
|
301
|
+
console.log(usage());
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const fail = (error, extra = {}) => {
|
|
306
|
+
if (parsed.json) console.log(JSON.stringify({ ok: false, error, ...extra }, null, 2));
|
|
307
|
+
else console.error(`link: ${error}`);
|
|
308
|
+
return 1;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
let report;
|
|
312
|
+
try {
|
|
313
|
+
report = deps.rosterReport ? deps.rosterReport(root) : require('./engine').rosterReport(root);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
return fail(`could not read the roster: ${error.message || error}`);
|
|
316
|
+
}
|
|
317
|
+
const worker = pickLinkWorker(report);
|
|
318
|
+
if (!worker) return fail('the roster has no build engine; run: atris engine roster');
|
|
319
|
+
|
|
320
|
+
const members = rosterMemberNames(report, root);
|
|
321
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
322
|
+
const notes = parsed.team
|
|
323
|
+
? members.map((name) => ({ name, note: teamNote(root, name, now) })).filter((entry) => entry.note)
|
|
324
|
+
: [];
|
|
325
|
+
const base = { things: parsed.things, context: parsed.context };
|
|
326
|
+
const prompt = notes.length
|
|
327
|
+
? fitTeamNotes(base, notes, members)
|
|
328
|
+
: buildLinkPrompt({ ...base, members, notes: [] });
|
|
329
|
+
|
|
330
|
+
const startedAt = new Date().toISOString();
|
|
331
|
+
const job = { engine: worker.engine, model: worker.model, ...(worker.effort ? { effort: worker.effort } : {}), prompt, label: 'link' };
|
|
332
|
+
const [answer] = await runEngineAskJobs([job], {
|
|
333
|
+
root,
|
|
334
|
+
concurrency: 1,
|
|
335
|
+
timeoutMs: deps.timeoutMs || LINK_TIMEOUT_MS,
|
|
336
|
+
executeAskJob: deps.executeAskJob || executeLinkJob,
|
|
337
|
+
});
|
|
338
|
+
if (!deps.executeAskJob) recordLinkRun(root, worker, answer, startedAt);
|
|
339
|
+
|
|
340
|
+
const ran = {
|
|
341
|
+
job: worker.job,
|
|
342
|
+
engine: worker.engine,
|
|
343
|
+
model: worker.model || null,
|
|
344
|
+
seconds: Math.round((Number(answer && answer.duration_ms) || 0) / 100) / 10,
|
|
345
|
+
team_notes: parsed.team ? notes.length : null,
|
|
346
|
+
};
|
|
347
|
+
if (!answer || !answer.ok) return fail(`the engine did not answer: ${compactFailure(answer)}`, { ran });
|
|
348
|
+
|
|
349
|
+
const result = normalizeLinkAnswer(extractJsonObject(answer.stdout), members);
|
|
350
|
+
if (!result.ok) return fail(result.error, { ran, raw: String(answer.stdout || '').trim().slice(0, 2000) });
|
|
351
|
+
|
|
352
|
+
if (parsed.json) console.log(JSON.stringify({ ...result.value, ran }, null, 2));
|
|
353
|
+
else printHuman(result.value, ran);
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = {
|
|
358
|
+
SYSTEM_PROMPT,
|
|
359
|
+
linkCommand,
|
|
360
|
+
parseLinkArgs,
|
|
361
|
+
pickLinkWorker,
|
|
362
|
+
teamNote,
|
|
363
|
+
fitTeamNotes,
|
|
364
|
+
extractJsonObject,
|
|
365
|
+
normalizeLinkAnswer,
|
|
366
|
+
linkInvocation,
|
|
367
|
+
};
|
package/commands/loops.js
CHANGED
|
@@ -276,7 +276,10 @@ function structuralProblems({ root, filePath, text, nowMs }) {
|
|
|
276
276
|
|
|
277
277
|
const owner = text.match(/\*\*Owner:\*\*\s*`team\/([^`]+)`/)?.[1]?.trim();
|
|
278
278
|
if (!owner) problems.push('no Owner field');
|
|
279
|
-
|
|
279
|
+
// Members live in atris/team/; older workspaces kept a root team/ folder.
|
|
280
|
+
else if (![path.join(root, 'atris', 'team', owner), path.join(root, 'team', owner)].some((dir) => fs.existsSync(dir))) {
|
|
281
|
+
problems.push(`owner team/${owner} does not exist`);
|
|
282
|
+
}
|
|
280
283
|
|
|
281
284
|
const wiki = text.match(/\*\*Wiki:\*\*\s*\[[^\]]+\]\(([^)]+)\)/)?.[1]?.trim();
|
|
282
285
|
if (!wiki) {
|
package/commands/member.js
CHANGED
|
@@ -9412,7 +9412,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9412
9412
|
// Subcommands that take a member name as args[0] otherwise treat `--help` as
|
|
9413
9413
|
// a name and error with "Member '--help' not found". `create`/`new` handle
|
|
9414
9414
|
// help themselves (with subcommand-specific usage), leave those alone.
|
|
9415
|
-
const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new', 'run', 'install']);
|
|
9415
|
+
const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new', 'run', 'install', 'result']);
|
|
9416
9416
|
if (!HELP_AWARE_SUBCOMMANDS.has(subcommand) && (args[0] === '-h' || args[0] === '--help')) {
|
|
9417
9417
|
subcommand = undefined;
|
|
9418
9418
|
}
|
|
@@ -9449,6 +9449,11 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9449
9449
|
return memberWake(args[0], ...args.slice(1));
|
|
9450
9450
|
case 'run':
|
|
9451
9451
|
return memberRun(args[0], ...args.slice(1));
|
|
9452
|
+
case 'result': {
|
|
9453
|
+
const code = require('../lib/member-result').memberResultCommand(args);
|
|
9454
|
+
if (code) process.exitCode = code;
|
|
9455
|
+
return undefined;
|
|
9456
|
+
}
|
|
9452
9457
|
case 'ping':
|
|
9453
9458
|
return memberPing(args[0], ...args.slice(1));
|
|
9454
9459
|
case 'loop':
|
|
@@ -9498,6 +9503,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9498
9503
|
console.log(' goal-from-score <name> Create/reuse an active goal from Team score evidence');
|
|
9499
9504
|
console.log(' wake <name> Read Mission state and decide tick/wait/ask/stop');
|
|
9500
9505
|
console.log(' run <name> ["..."] Start a budgeted member mission, or run its active Mission Runtime');
|
|
9506
|
+
console.log(' result <name> Newest run result: run record, receipt, artifacts, number [--since <iso>] [--json]');
|
|
9501
9507
|
console.log(' loop <name|--all> Repeat wake on a bounded cadence with a no-overlap lease');
|
|
9502
9508
|
console.log(' tick <name> Propose the next bounded experiment');
|
|
9503
9509
|
console.log(' review <name> <id> Accept/discard an experiment with proof');
|
|
@@ -9533,6 +9539,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9533
9539
|
console.log(' atris member wake growth --json');
|
|
9534
9540
|
console.log(' atris member run growth "improve onboarding proof" --verify "npm test" --minutes 30 --json');
|
|
9535
9541
|
console.log(' atris member run growth --mission <mission-id> --max-ticks 1 --json');
|
|
9542
|
+
console.log(' atris member result growth --json');
|
|
9536
9543
|
console.log(' atris member wake growth --execute --confirm-autonomy-policy');
|
|
9537
9544
|
console.log(' atris member supervisor recommendations --json');
|
|
9538
9545
|
console.log(' atris member objective-generator proposals --json');
|
package/commands/orb.js
CHANGED
|
@@ -42,7 +42,7 @@ function parseEngine(args) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function engineInvocation(engine, prompt) {
|
|
45
|
-
if (engine === 'codex') return { command: 'codex', args: ['exec', prompt] };
|
|
45
|
+
if (engine === 'codex') return { command: 'codex', args: ['exec', '--skip-git-repo-check', prompt] };
|
|
46
46
|
if (engine === 'fast') return { command: 'ax', args: ['--fast', '--print', prompt] };
|
|
47
47
|
return { command: 'claude', args: ['-p', prompt] };
|
|
48
48
|
}
|
package/commands/xp.js
CHANGED
|
@@ -933,6 +933,8 @@ function receiptFromTaskEpisode(episode, treeHash) {
|
|
|
933
933
|
source_episode_id: episodeId,
|
|
934
934
|
workspace_root: episode.workspace_root || null,
|
|
935
935
|
actor: episode.action?.actor || null,
|
|
936
|
+
// Who built the work. `actor` is the reviewer; trust is earned by the builder.
|
|
937
|
+
builder: episode.state?.metadata?.built_by || episode.state?.claimed_by || null,
|
|
936
938
|
outcome: rejected ? 'rejected' : 'accepted',
|
|
937
939
|
xp: rejected ? 0 : reward,
|
|
938
940
|
reward: rejected ? 0 : reward,
|
package/commands/youtube.js
CHANGED
|
@@ -1229,7 +1229,7 @@ function buildDigestPrompt(briefs = []) {
|
|
|
1229
1229
|
|
|
1230
1230
|
function defaultDigestRunner(prompt, deps = {}) {
|
|
1231
1231
|
const spawn = deps.spawnSync || spawnSync;
|
|
1232
|
-
return spawn('claude', ['-p', prompt, '--model', 'claude-haiku-
|
|
1232
|
+
return spawn('claude', ['-p', prompt, '--model', 'claude-haiku-5-5'], {
|
|
1233
1233
|
encoding: 'utf8',
|
|
1234
1234
|
timeout: DIGEST_ENGINE_TIMEOUT_MS,
|
|
1235
1235
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -6,7 +6,8 @@ const path = require('path');
|
|
|
6
6
|
const { taskProofState, taskProofExecutionState } = require('./task-proof');
|
|
7
7
|
const { extractReceiptEvidence } = require('./receipt-evidence');
|
|
8
8
|
const reviewIntegrity = require('./review-integrity');
|
|
9
|
-
const { computeTrustTier } = require('./trust-tiers');
|
|
9
|
+
const { computeTrustTier, taskTrustSubject } = require('./trust-tiers');
|
|
10
|
+
const { rewriteWorktreeVerify, missingOnMain } = require('./verify-worktree-rewrite');
|
|
10
11
|
const { policyLessonsForFiles, readPolicyLessons } = require('./policy-lessons');
|
|
11
12
|
const { skillEvalGate } = require('./skill-eval-gate');
|
|
12
13
|
|
|
@@ -1091,6 +1092,79 @@ function sniffedProtectedLane(task) {
|
|
|
1091
1092
|
return null;
|
|
1092
1093
|
}
|
|
1093
1094
|
|
|
1095
|
+
// The protected lane a task sits in, or null: a denied tag (whole word,
|
|
1096
|
+
// plural-stripped, the same match the landing gate uses), a lane declared in
|
|
1097
|
+
// its text, or, for untagged rows, lane terms in its title or objective.
|
|
1098
|
+
// Every automatic closer must leave these for a human.
|
|
1099
|
+
function deniedTagOf(task) {
|
|
1100
|
+
const tag = String((task && task.tag) || '').trim().toLowerCase();
|
|
1101
|
+
return [...DENIED_TAGS].find((d) =>
|
|
1102
|
+
tag === d || tag.split(/[^a-z0-9]+/).some((w) => w === d || w.replace(/s$/, '') === d)) || null;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function protectedLaneOf(task) {
|
|
1106
|
+
if (!task || typeof task !== 'object') return null;
|
|
1107
|
+
const metadata = task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
1108
|
+
const view = { ...task, objective: task.objective || metadata.objective || '', metadata };
|
|
1109
|
+
const denied = deniedTagOf(view);
|
|
1110
|
+
if (denied) return denied;
|
|
1111
|
+
if (declaredProtectedLane(view)) return 'declared';
|
|
1112
|
+
if (!String(view.tag || '').trim()) {
|
|
1113
|
+
const sniffed = sniffedProtectedLane(view);
|
|
1114
|
+
if (sniffed) return sniffed.lane;
|
|
1115
|
+
}
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// The check the landing pass actually runs. A check recorded inside an
|
|
1120
|
+
// agent's temporary copy is rewritten to run from the main checkout; when the
|
|
1121
|
+
// file it runs only exists on the agent's branch, that is reported in plain
|
|
1122
|
+
// words instead of a bare "not on the list this machine may run".
|
|
1123
|
+
function resolveTaskVerify(task) {
|
|
1124
|
+
const recorded = String((task.metadata || {}).verify || '').trim();
|
|
1125
|
+
if (!recorded) return { verify: '', recorded: '', rewritten: false, missing: null };
|
|
1126
|
+
const root = task.workspace_root || process.cwd();
|
|
1127
|
+
const rewrite = rewriteWorktreeVerify(recorded, root);
|
|
1128
|
+
const verify = rewrite.command || recorded;
|
|
1129
|
+
let missing = null;
|
|
1130
|
+
if (rewrite.rewritten) {
|
|
1131
|
+
const chain = parseVerifyChain(verify);
|
|
1132
|
+
if (chain.ok) missing = missingOnMain(chain.steps, root);
|
|
1133
|
+
}
|
|
1134
|
+
return { verify, recorded, rewritten: rewrite.rewritten, missing };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Why the landing pass could never rerun this task's check from main, or
|
|
1138
|
+
// null when it can. Independent of the gate's refusal order, so a row held
|
|
1139
|
+
// for another reason still reports a check that can never run.
|
|
1140
|
+
function verifyRunnability(task) {
|
|
1141
|
+
const resolved = resolveTaskVerify(task);
|
|
1142
|
+
if (!resolved.verify) return 'no_verify_command';
|
|
1143
|
+
if (!isAutoCertifyVerifyCommandAllowed(resolved.verify)) return 'verify_command_not_allowed';
|
|
1144
|
+
if (resolved.missing) return 'verify_only_on_agent_branch';
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function agentBranchOnlyResult(ref, resolved) {
|
|
1149
|
+
return {
|
|
1150
|
+
eligible: false,
|
|
1151
|
+
ref,
|
|
1152
|
+
reason: 'verify_only_on_agent_branch',
|
|
1153
|
+
verify: resolved.verify,
|
|
1154
|
+
recorded_verify: resolved.recorded,
|
|
1155
|
+
missing_path: resolved.missing,
|
|
1156
|
+
next_action: `${resolved.missing} is not on main yet; land the agent branch that has it, then the check reruns from main`,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function runResolvedVerify(resolved, workspaceRoot, cache) {
|
|
1161
|
+
const result = runVerifyCommandCached(resolved.verify, workspaceRoot, cache);
|
|
1162
|
+
if (resolved.rewritten && result.reason === 'verify_failed') {
|
|
1163
|
+
return { ...result, reason: 'verify_failed_on_main', recorded_verify: resolved.recorded };
|
|
1164
|
+
}
|
|
1165
|
+
return resolved.rewritten ? { ...result, recorded_verify: resolved.recorded } : result;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1094
1168
|
function evaluateAutoAccept(task, options = {}) {
|
|
1095
1169
|
const {
|
|
1096
1170
|
strictVerify = true,
|
|
@@ -1103,10 +1177,9 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1103
1177
|
if (task.status !== 'review') return { eligible: false, ref, reason: 'not_in_review' };
|
|
1104
1178
|
const metadata = task.metadata || {};
|
|
1105
1179
|
const review = task.review || {};
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
);
|
|
1180
|
+
// Trust is the builder's, never the approver's: a task built by a member
|
|
1181
|
+
// with a clean record lands without waiting on a human look.
|
|
1182
|
+
const trustTier = computeTrustTier(taskTrustSubject(task), task.workspace_root || process.cwd());
|
|
1110
1183
|
const tierRequiresStrictVerify = trustTier !== 'trusted';
|
|
1111
1184
|
const requiresStrictVerify = strictVerify || tierRequiresStrictVerify;
|
|
1112
1185
|
const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
|
|
@@ -1180,10 +1253,12 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1180
1253
|
}
|
|
1181
1254
|
const evidenceBlock = receiptEvidenceBlock(task, ref);
|
|
1182
1255
|
if (evidenceBlock) return evidenceBlock;
|
|
1183
|
-
const
|
|
1256
|
+
const resolvedVerify = resolveTaskVerify(task);
|
|
1257
|
+
const verify = resolvedVerify.verify;
|
|
1184
1258
|
if (tierRequiresStrictVerify && !verify) return strictVerifyMissingResult(ref);
|
|
1259
|
+
if (resolvedVerify.missing) return agentBranchOnlyResult(ref, resolvedVerify);
|
|
1185
1260
|
if (verify && executeVerify) {
|
|
1186
|
-
const verifyResult =
|
|
1261
|
+
const verifyResult = runResolvedVerify(resolvedVerify, task.workspace_root || process.cwd(), verifyCache);
|
|
1187
1262
|
// A check that runs and fails blocks. So does a check whose worktree
|
|
1188
1263
|
// is gone, otherwise the daily reap converts "has a failing check"
|
|
1189
1264
|
// into "lands unchecked" the morning after it clears the worktree.
|
|
@@ -1194,6 +1269,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1194
1269
|
// "the harness could not run the check" into "landed unchecked".
|
|
1195
1270
|
if ((tierRequiresStrictVerify && !verifyResult.ok)
|
|
1196
1271
|
|| verifyResult.reason === 'verify_failed'
|
|
1272
|
+
|| verifyResult.reason === 'verify_failed_on_main'
|
|
1197
1273
|
|| verifyResult.reason === 'verify_unrunnable'
|
|
1198
1274
|
|| verifyResult.reason === 'verify_worktree_missing'
|
|
1199
1275
|
|| verifyResult.reason === 'verify_workdir_missing') {
|
|
@@ -1279,11 +1355,13 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1279
1355
|
}
|
|
1280
1356
|
|
|
1281
1357
|
if (requiresStrictVerify) {
|
|
1282
|
-
const
|
|
1358
|
+
const resolvedVerify = resolveTaskVerify(task);
|
|
1359
|
+
const verify = resolvedVerify.verify;
|
|
1283
1360
|
if (!verify) return strictVerifyMissingResult(ref);
|
|
1361
|
+
if (resolvedVerify.missing) return agentBranchOnlyResult(ref, resolvedVerify);
|
|
1284
1362
|
if (executeVerify) {
|
|
1285
1363
|
const workspaceRoot = task.workspace_root || process.cwd();
|
|
1286
|
-
const verifyResult =
|
|
1364
|
+
const verifyResult = runResolvedVerify(resolvedVerify, workspaceRoot, verifyCache);
|
|
1287
1365
|
if (!verifyResult.ok) {
|
|
1288
1366
|
return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
|
|
1289
1367
|
}
|
|
@@ -1321,6 +1399,8 @@ module.exports = {
|
|
|
1321
1399
|
candidatePolicyGate,
|
|
1322
1400
|
declaredProtectedLane,
|
|
1323
1401
|
sniffedProtectedLane,
|
|
1402
|
+
protectedLaneOf,
|
|
1403
|
+
verifyRunnability,
|
|
1324
1404
|
evaluateAutoAccept,
|
|
1325
1405
|
isAutoCertifyVerifyCommandAllowed,
|
|
1326
1406
|
parseVerifyChain,
|