atris 3.43.0 → 3.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/atris/skills/design/SKILL.md +7 -1
  2. package/atris/skills/engines/SKILL.md +44 -13
  3. package/atris/team/customer-lead/MEMBER.md +45 -0
  4. package/atris/team/customer-lead/SOUL.md +33 -0
  5. package/atris/team/customer-lead/START_HERE.md +7 -0
  6. package/atris/team/customer-lead/skills/customer-commitments/SKILL.md +45 -0
  7. package/atris/team/improver/MEMBER.md +33 -0
  8. package/bin/atris.js +36 -3
  9. package/commands/aeo.js +5 -2
  10. package/commands/align.js +5 -2
  11. package/commands/autoland.js +15 -1
  12. package/commands/caretaker.js +303 -0
  13. package/commands/clean.js +76 -0
  14. package/commands/computer.js +5 -2
  15. package/commands/engine-watch.js +212 -0
  16. package/commands/engine.js +99 -11
  17. package/commands/founder.js +304 -0
  18. package/commands/human-missions.js +844 -0
  19. package/commands/improve.js +29 -6
  20. package/commands/init.js +16 -7
  21. package/commands/mission.js +124 -69
  22. package/commands/pull.js +5 -2
  23. package/commands/push.js +5 -2
  24. package/commands/slop.js +34 -3
  25. package/commands/task.js +51 -4
  26. package/commands/team.js +329 -13
  27. package/commands/terminal.js +5 -2
  28. package/commands/verify.js +99 -6
  29. package/commands/workflow.js +10 -3
  30. package/commands/worktree.js +119 -4
  31. package/lib/auto-accept-certified.js +302 -0
  32. package/lib/cloud-mission.js +59 -2
  33. package/lib/conductor-artifacts.js +1 -1
  34. package/lib/dispatch-scout.js +386 -0
  35. package/lib/engine-ask.js +645 -0
  36. package/lib/engine-job-lifecycle.js +65 -0
  37. package/lib/engine-receipt-sweep.js +98 -0
  38. package/lib/engine-registry.js +2 -2
  39. package/lib/engine-validate.js +382 -0
  40. package/lib/fleet.js +459 -106
  41. package/lib/known-commands.js +2 -2
  42. package/lib/member-alive.js +2 -2
  43. package/lib/policy-lessons.js +70 -0
  44. package/lib/receipt-evidence.js +56 -1
  45. package/lib/runner-command.js +1 -1
  46. package/lib/secret-gateway.js +588 -0
  47. package/lib/team-presence.js +13 -1
  48. package/lib/voice-gate.js +6 -0
  49. package/lib/wish-audit.js +5 -205
  50. package/lib/wish-delegate.js +5 -2
  51. package/package.json +6 -1
  52. package/utils/auth.js +56 -9
@@ -0,0 +1,386 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const {
7
+ buildReadOnlyEngineInvocation,
8
+ runAskProcess,
9
+ runEngineAskJobs,
10
+ } = require('./engine-ask');
11
+
12
+ const SCOUT_PACK_SCHEMA = 'atris.dispatch_scout_pack.v1';
13
+ const SCOUT_ENGINE = 'haiku';
14
+ // Real haiku pack builds measured 59s/66s/151s on 2026-08-12; 45s dropped
15
+ // nearly every live run. Scout stays optional, so a longer wait only delays
16
+ // one build's start, never blocks it.
17
+ const SCOUT_TIMEOUT_MS = 150000;
18
+ const SCOUT_MAX_HITS = 8;
19
+ const SCOUT_MAX_MAP_GOTCHAS = 4;
20
+ const SCOUT_MAX_EXCERPT_LINES = 12;
21
+ const SCOUT_MAX_PACK_BYTES = 4096;
22
+ const SCOUT_MAX_ALLOWED_FILES = 48;
23
+ const SCOUT_BLOCK_HEADING = '## verified starting points for this commit';
24
+
25
+ const TITLE_STOP_WORDS = new Set([
26
+ 'after', 'against', 'before', 'brief', 'build', 'builder', 'builders', 'building',
27
+ 'check', 'done', 'exactly', 'first', 'from', 'have', 'instead', 'into', 'minutes',
28
+ 'only', 'should', 'stop', 'task', 'that', 'their', 'then', 'this', 'through',
29
+ 'verified', 'with', 'without', 'work', 'working',
30
+ ]);
31
+
32
+ function git(root, args, options = {}) {
33
+ return spawnSync('git', ['-C', root, ...args], {
34
+ encoding: 'utf8',
35
+ maxBuffer: 4 * 1024 * 1024,
36
+ timeout: 5000,
37
+ ...options,
38
+ });
39
+ }
40
+
41
+ function checkoutCommit(root) {
42
+ const result = git(root, ['rev-parse', 'HEAD']);
43
+ if (result.status !== 0) return '';
44
+ const commit = String(result.stdout || '').trim();
45
+ return /^[0-9a-f]{40}$/i.test(commit) ? commit : '';
46
+ }
47
+
48
+ function trackedFiles(root) {
49
+ const result = git(root, ['ls-files', '-z']);
50
+ if (result.status !== 0) return [];
51
+ return String(result.stdout || '').split('\0').map((entry) => entry.trim()).filter(Boolean);
52
+ }
53
+
54
+ function taskRef(task) {
55
+ return String(task && (task.display_id || task.task_id || task.id) || '').trim();
56
+ }
57
+
58
+ function titleKeywords(title) {
59
+ const seen = new Set();
60
+ const out = [];
61
+ for (const token of String(title || '').toLowerCase().match(/[a-z0-9][a-z0-9_-]*/g) || []) {
62
+ if (token.length < 4 || TITLE_STOP_WORDS.has(token) || seen.has(token)) continue;
63
+ seen.add(token);
64
+ out.push(token);
65
+ if (out.length === 12) break;
66
+ }
67
+ return out;
68
+ }
69
+
70
+ function mapPathRefs(line, tracked) {
71
+ const refs = [];
72
+ const tokens = String(line || '').match(/[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*\.[A-Za-z0-9_.-]+/g) || [];
73
+ for (const token of tokens) {
74
+ const normalized = token.replace(/^\.\//, '');
75
+ if (tracked.has(normalized) && !refs.includes(normalized)) refs.push(normalized);
76
+ }
77
+ return refs;
78
+ }
79
+
80
+ function keywordFileMatches(root, keywords) {
81
+ if (!keywords.length) return [];
82
+ const args = ['grep', '-I', '-l', '-F', '-i'];
83
+ for (const keyword of keywords) args.push('-e', keyword);
84
+ args.push('--');
85
+ const result = git(root, args);
86
+ if (result.status !== 0 && result.status !== 1) return [];
87
+ return String(result.stdout || '').split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
88
+ }
89
+
90
+ function seedScoutContext({ task, worktreePath }) {
91
+ const trackedList = trackedFiles(worktreePath);
92
+ const tracked = new Set(trackedList);
93
+ const keywords = titleKeywords(task && task.title);
94
+ const mapPath = 'atris/MAP.md';
95
+ let mapText = '';
96
+ try { mapText = fs.readFileSync(path.join(worktreePath, mapPath), 'utf8'); } catch {}
97
+
98
+ const scores = new Map();
99
+ const gotchaCandidates = [];
100
+ const addScore = (file, score) => {
101
+ if (!tracked.has(file)) return;
102
+ scores.set(file, Math.max(scores.get(file) || 0, score));
103
+ };
104
+ if (tracked.has(mapPath)) addScore(mapPath, 1);
105
+
106
+ const mapLines = mapText.split(/\r?\n/);
107
+ for (let index = 0; index < mapLines.length; index += 1) {
108
+ const line = mapLines[index];
109
+ const lower = line.toLowerCase();
110
+ const matches = keywords.filter((keyword) => lower.includes(keyword));
111
+ const refs = mapPathRefs(line, tracked);
112
+ for (const ref of refs) {
113
+ const pathMatches = keywords.filter((keyword) => ref.toLowerCase().includes(keyword)).length;
114
+ if (matches.length || pathMatches) addScore(ref, 80 + (matches.length * 5) + (pathMatches * 8));
115
+ }
116
+ if (matches.length && line.trim() && line.trim().length <= 300 && gotchaCandidates.length < 16) {
117
+ gotchaCandidates.push({ line: index + 1, text: line.trim() });
118
+ }
119
+ }
120
+
121
+ for (const file of trackedList) {
122
+ const pathMatches = keywords.filter((keyword) => file.toLowerCase().includes(keyword)).length;
123
+ if (pathMatches) addScore(file, 60 + (pathMatches * 8));
124
+ }
125
+ for (const file of keywordFileMatches(worktreePath, keywords)) addScore(file, 50);
126
+
127
+ const allowedFiles = [...scores]
128
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
129
+ .slice(0, SCOUT_MAX_ALLOWED_FILES)
130
+ .map(([file]) => file);
131
+ return { allowedFiles, gotchaCandidates, keywords };
132
+ }
133
+
134
+ function scoutPrompt({ task, commit, seed }) {
135
+ return [
136
+ `Rank verified starting points for task ${taskRef(task)} at checkout commit ${commit}.`,
137
+ `Task title: ${String(task && task.title || '').trim().slice(0, 800)}`,
138
+ 'Read atris/MAP.md first. Inspect only the allowed tracked files below. Do not cite or mention any other file.',
139
+ 'Return one JSON object and no prose. Use this exact shape:',
140
+ JSON.stringify({
141
+ schema: SCOUT_PACK_SCHEMA,
142
+ task_id: taskRef(task),
143
+ checkout_commit: commit,
144
+ hits: [{ path: 'lib/example.js', line: 12, excerpt: 'verbatim source, at most 12 lines', why: 'One sentence.' }],
145
+ map_gotchas: [{ line: 1, text: 'verbatim MAP line from the candidate list' }],
146
+ not_checked: ['short gap'],
147
+ }),
148
+ `Return at most ${SCOUT_MAX_HITS} hits and ${SCOUT_MAX_MAP_GOTCHAS} MAP gotchas. Every excerpt must be verbatim source and at most ${SCOUT_MAX_EXCERPT_LINES} lines.`,
149
+ 'Allowed tracked files:',
150
+ ...seed.allowedFiles.map((file) => `- ${file}`),
151
+ 'MAP gotcha candidates, choose only exact objects from this list:',
152
+ JSON.stringify(seed.gotchaCandidates),
153
+ ].join('\n');
154
+ }
155
+
156
+ function buildScoutInvocation(job) {
157
+ const invocation = buildReadOnlyEngineInvocation(job.engine, job.prompt, job.model);
158
+ const args = [...invocation.args];
159
+ const toolsIndex = args.indexOf('--tools');
160
+ if (toolsIndex !== -1) args[toolsIndex + 1] = 'Read,Glob,Grep';
161
+ return { ...invocation, args };
162
+ }
163
+
164
+ async function defaultScoutAsk({ job, root, timeoutMs }) {
165
+ const answers = await runEngineAskJobs([job], {
166
+ root,
167
+ concurrency: 1,
168
+ timeoutMs,
169
+ executeAskJob: async (askJob) => {
170
+ const invocation = buildScoutInvocation(askJob);
171
+ return runAskProcess(invocation, {
172
+ cwd: invocation.cwd || root,
173
+ timeoutMs,
174
+ });
175
+ },
176
+ });
177
+ return answers[0] || null;
178
+ }
179
+
180
+ function firstJsonObject(text) {
181
+ const source = String(text || '').trim();
182
+ try { return JSON.parse(source); } catch {}
183
+ let start = -1;
184
+ let depth = 0;
185
+ let inString = false;
186
+ let escaped = false;
187
+ for (let index = 0; index < source.length; index += 1) {
188
+ const char = source[index];
189
+ if (inString) {
190
+ if (escaped) escaped = false;
191
+ else if (char === '\\') escaped = true;
192
+ else if (char === '"') inString = false;
193
+ continue;
194
+ }
195
+ if (char === '"') { inString = true; continue; }
196
+ if (char === '{') {
197
+ if (depth === 0) start = index;
198
+ depth += 1;
199
+ continue;
200
+ }
201
+ if (char !== '}' || depth === 0) continue;
202
+ depth -= 1;
203
+ if (depth === 0 && start !== -1) {
204
+ try { return JSON.parse(source.slice(start, index + 1)); } catch { start = -1; }
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+
210
+ function lineNumberAt(text, offset) {
211
+ let line = 1;
212
+ for (let index = 0; index < offset; index += 1) if (text.charCodeAt(index) === 10) line += 1;
213
+ return line;
214
+ }
215
+
216
+ function excerptLocations(source, excerpt) {
217
+ const locations = [];
218
+ let offset = source.indexOf(excerpt);
219
+ while (offset !== -1) {
220
+ locations.push({ offset, line: lineNumberAt(source, offset) });
221
+ offset = source.indexOf(excerpt, offset + 1);
222
+ }
223
+ return locations;
224
+ }
225
+
226
+ function excerptSymbol(excerpt) {
227
+ const source = String(excerpt || '');
228
+ const declared = source.match(/\b(?:async\s+function|function|class|const|let|var)\s+([A-Za-z_$][\w$]*)/);
229
+ if (declared) return declared[1];
230
+ const called = source.match(/\b([A-Za-z_$][\w$]*)\s*\(/);
231
+ return called ? called[1] : '';
232
+ }
233
+
234
+ function normalizeWhy(value) {
235
+ const why = String(value || '').trim();
236
+ if (!why || why.length > 240 || /[\r\n]/.test(why)) return '';
237
+ return why;
238
+ }
239
+
240
+ function verifyHit(hit, { worktreePath, tracked, allowed }) {
241
+ if (!hit || typeof hit !== 'object' || Array.isArray(hit)) return null;
242
+ const citedPath = String(hit.path || '').trim().replace(/^\.\//, '');
243
+ if (!citedPath || path.isAbsolute(citedPath) || citedPath.split('/').includes('..')) return null;
244
+ if (!tracked.has(citedPath) || !allowed.has(citedPath)) return null;
245
+ const absolutePath = path.join(worktreePath, citedPath);
246
+ let stat;
247
+ let source;
248
+ try {
249
+ stat = fs.lstatSync(absolutePath);
250
+ source = fs.readFileSync(absolutePath, 'utf8');
251
+ } catch {
252
+ return null;
253
+ }
254
+ if (!stat.isFile() || source.includes('\0')) return null;
255
+ const excerpt = String(hit.excerpt || '');
256
+ const excerptLines = excerpt.split('\n');
257
+ const citedLine = Number(hit.line);
258
+ const why = normalizeWhy(hit.why);
259
+ if (!excerpt || excerptLines.length > SCOUT_MAX_EXCERPT_LINES || !Number.isInteger(citedLine) || citedLine < 1 || !why) return null;
260
+ const locations = excerptLocations(source, excerpt);
261
+ if (!locations.length) return null;
262
+ locations.sort((left, right) => Math.abs(left.line - citedLine) - Math.abs(right.line - citedLine));
263
+ const nearest = locations[0];
264
+ if (Math.abs(nearest.line - citedLine) > SCOUT_MAX_EXCERPT_LINES) {
265
+ const symbol = excerptSymbol(excerpt);
266
+ if (!symbol || !source.slice(nearest.offset, nearest.offset + excerpt.length).includes(symbol)) return null;
267
+ }
268
+ return { path: citedPath, line: nearest.line, excerpt, why };
269
+ }
270
+
271
+ function verifyMapGotcha(gotcha, mapText) {
272
+ if (!gotcha || typeof gotcha !== 'object' || Array.isArray(gotcha)) return null;
273
+ const text = String(gotcha.text || '').trim();
274
+ const citedLine = Number(gotcha.line);
275
+ if (!text || text.length > 300 || !Number.isInteger(citedLine) || citedLine < 1) return null;
276
+ const lines = mapText.split(/\r?\n/);
277
+ const actualLine = lines.findIndex((line) => line.trim() === text) + 1;
278
+ if (!actualLine) return null;
279
+ return { line: actualLine, text };
280
+ }
281
+
282
+ function verifyScoutPack(rawPack, { task, worktreePath, allowedFiles, expectedCommit = '' }) {
283
+ if (!rawPack || typeof rawPack !== 'object' || Array.isArray(rawPack)) return null;
284
+ const currentCommit = checkoutCommit(worktreePath);
285
+ const commit = String(rawPack.checkout_commit || '').trim();
286
+ if (!currentCommit || !commit || commit !== currentCommit || (expectedCommit && commit !== expectedCommit)) return null;
287
+ if (rawPack.schema !== SCOUT_PACK_SCHEMA || String(rawPack.task_id || '') !== taskRef(task)) return null;
288
+
289
+ const tracked = new Set(trackedFiles(worktreePath));
290
+ const allowed = new Set((allowedFiles || []).filter((file) => tracked.has(file)));
291
+ const verifiedHits = (Array.isArray(rawPack.hits) ? rawPack.hits : [])
292
+ .slice(0, SCOUT_MAX_HITS)
293
+ .map((hit) => verifyHit(hit, { worktreePath, tracked, allowed }))
294
+ .filter(Boolean);
295
+ const seenHits = new Set();
296
+ const hits = verifiedHits.filter((hit) => {
297
+ const key = `${hit.path}\0${hit.line}\0${hit.excerpt}`;
298
+ if (seenHits.has(key)) return false;
299
+ seenHits.add(key);
300
+ return true;
301
+ });
302
+ if (hits.length < 2) return null;
303
+
304
+ let mapText = '';
305
+ try { mapText = fs.readFileSync(path.join(worktreePath, 'atris', 'MAP.md'), 'utf8'); } catch {}
306
+ const mapGotchas = (Array.isArray(rawPack.map_gotchas) ? rawPack.map_gotchas : [])
307
+ .slice(0, SCOUT_MAX_MAP_GOTCHAS)
308
+ .map((gotcha) => verifyMapGotcha(gotcha, mapText))
309
+ .filter(Boolean);
310
+ const notChecked = (Array.isArray(rawPack.not_checked) ? rawPack.not_checked : [])
311
+ .map((entry) => String(entry || '').trim())
312
+ .filter((entry) => entry && entry.length <= 160 && !/[\r\n]/.test(entry))
313
+ .slice(0, 4);
314
+ const pack = {
315
+ schema: SCOUT_PACK_SCHEMA,
316
+ task_id: taskRef(task),
317
+ checkout_commit: commit,
318
+ hits,
319
+ map_gotchas: mapGotchas,
320
+ not_checked: notChecked,
321
+ };
322
+ return Buffer.byteLength(JSON.stringify(pack)) <= SCOUT_MAX_PACK_BYTES ? pack : null;
323
+ }
324
+
325
+ async function buildVerifiedScoutPack({ task, worktreePath, ask = defaultScoutAsk }) {
326
+ try {
327
+ const commit = checkoutCommit(worktreePath);
328
+ if (!commit) return null;
329
+ const seed = seedScoutContext({ task, worktreePath });
330
+ if (seed.allowedFiles.length < 2) return null;
331
+ const job = {
332
+ engine: SCOUT_ENGINE,
333
+ model: '',
334
+ label: 'dispatch-scout',
335
+ prompt: scoutPrompt({ task, commit, seed }),
336
+ };
337
+ const answer = await ask({ job, root: worktreePath, timeoutMs: SCOUT_TIMEOUT_MS });
338
+ if (!answer || answer.ok !== true || answer.timed_out || answer.cancelled) return null;
339
+ const rawPack = firstJsonObject(answer.stdout);
340
+ return verifyScoutPack(rawPack, {
341
+ task,
342
+ worktreePath,
343
+ allowedFiles: seed.allowedFiles,
344
+ expectedCommit: commit,
345
+ });
346
+ } catch {
347
+ return null;
348
+ }
349
+ }
350
+
351
+ function renderVerifiedScoutBlock(pack, { worktreePath }) {
352
+ if (!pack || checkoutCommit(worktreePath) !== pack.checkout_commit) return '';
353
+ const lines = [
354
+ SCOUT_BLOCK_HEADING,
355
+ `These starting points were verified against commit ${pack.checkout_commit}. Open atris/MAP.md first. If a named symbol is missing, ignore this block and read MAP.`,
356
+ ];
357
+ for (const hit of pack.hits) {
358
+ lines.push(`- ${hit.path}:${hit.line} ${hit.why}`);
359
+ lines.push(...hit.excerpt.split('\n').map((line) => ` ${line}`));
360
+ }
361
+ if (pack.map_gotchas.length) {
362
+ lines.push('MAP gotchas:');
363
+ lines.push(...pack.map_gotchas.map((gotcha) => `- MAP line ${gotcha.line}: ${gotcha.text}`));
364
+ }
365
+ if (pack.not_checked.length) lines.push(`Not checked: ${pack.not_checked.join('; ')}`);
366
+ return lines.join('\n');
367
+ }
368
+
369
+ function appendVerifiedScoutPack(brief, pack, { worktreePath }) {
370
+ const source = String(brief || '');
371
+ if (!pack || source.includes(SCOUT_BLOCK_HEADING)) return source;
372
+ const block = renderVerifiedScoutBlock(pack, { worktreePath });
373
+ return block ? `${source}\n\n${block}` : source;
374
+ }
375
+
376
+ module.exports = {
377
+ SCOUT_PACK_SCHEMA,
378
+ SCOUT_TIMEOUT_MS,
379
+ SCOUT_BLOCK_HEADING,
380
+ checkoutCommit,
381
+ seedScoutContext,
382
+ buildScoutInvocation,
383
+ verifyScoutPack,
384
+ buildVerifiedScoutPack,
385
+ appendVerifiedScoutPack,
386
+ };