atris 3.58.7 → 3.58.8
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/GETTING_STARTED.md +2 -2
- package/PERSONA.md +11 -11
- package/README.md +1 -1
- package/atris/GEMINI.md +7 -6
- package/atris/PERSONA.md +4 -4
- package/atris/features/README.md +77 -6
- package/atris/skills/README.md +44 -15
- package/atris/skills/aeo/SKILL.md +10 -10
- package/atris/skills/atris/SKILL.md +1 -1
- package/atris/skills/engines/SKILL.md +42 -50
- package/atris/skills/meta/SKILL.md +4 -4
- package/bin/atris.js +5 -6
- package/commands/aeo.js +9 -12
- package/commands/analytics.js +8 -11
- package/commands/autopilot.js +4 -3
- package/commands/avail.js +1 -1
- package/commands/brain.js +3 -1
- package/commands/business.js +13 -8
- package/commands/close.js +2 -1
- package/commands/computer.js +15 -10
- package/commands/context-sync.js +3 -3
- package/commands/doc-health.js +50 -2
- package/commands/experiments.js +13 -4
- package/commands/improve.js +1 -1
- package/commands/integrations.js +2 -5
- package/commands/learn.js +1 -1
- package/commands/member.js +174 -47
- package/commands/mission.js +57 -11
- package/commands/pack.js +2 -2
- package/commands/probe.js +11 -5
- package/commands/pull.js +1 -1
- package/commands/push.js +1 -1
- package/commands/rainmaker.js +3 -16
- package/commands/spaceship.js +1 -1
- package/commands/stream.js +27 -2
- package/commands/study.js +15 -9
- package/commands/sync.js +1 -1
- package/commands/task.js +13 -2
- package/commands/workflow.js +6 -1
- package/lib/codex-flight.js +28 -5
- package/lib/fleet.js +4 -1
- package/lib/improve-vitals-html.js +3 -2
- package/lib/lesson-contradiction.js +2 -2
- package/lib/map-refs.js +183 -0
- package/lib/mission-room.js +4 -4
- package/lib/noninteractive.js +15 -4
- package/lib/policy-lessons.js +1 -1
- package/lib/receipt-block.js +137 -28
- package/lib/runner-command.js +1 -1
- package/lib/scorecard.js +4 -4
- package/lib/sleep-sync.js +11 -0
- package/lib/state-detection.js +28 -9
- package/lib/task-db.js +3 -2
- package/lib/todo-fallback.js +1 -1
- package/lib/wish-delegate.js +19 -5
- package/mcp/atris-mcp/index.mjs +77 -24
- package/package.json +2 -5
- package/scripts/det/det.js +2 -2
- package/scripts/det/pr-description.js +2 -2
- package/scripts/det/test.js +2 -2
- package/utils/api.js +1 -1
- package/utils/auth.js +17 -7
- package/utils/backend-root.js +31 -0
- package/utils/config.js +5 -4
- package/utils/owner-identity.js +61 -0
- package/atris/policies/LESSONS.md +0 -13
- package/atris/skills/autopilot/atris-autopilot.md +0 -71
package/lib/receipt-block.js
CHANGED
|
@@ -98,18 +98,116 @@ function directProofText(value) {
|
|
|
98
98
|
return ran ? `${ran[1]} passed` : text;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// A receipt is a text from a good employee: one sentence a friend would send,
|
|
102
|
+
// plus one link a stranger can check. Ids, statuses, scores and verifier
|
|
103
|
+
// output stay underneath (they feed the rating); they never lead the surface.
|
|
104
|
+
// Standard: atris-labs atris/wiki/systems/receipt-standard.md
|
|
105
|
+
|
|
106
|
+
function sentence(value) {
|
|
107
|
+
const text = trimPunctuation(value);
|
|
108
|
+
if (!text) return '';
|
|
109
|
+
return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Bracketed IPv6 hosts need their own alternative: the plain class stops at
|
|
113
|
+
// ] and would cut http://[::1]:3000 down to http://[::1 .
|
|
114
|
+
const URL_RE = /https?:\/\/\[[0-9a-f.:]+\][^\s)>\]]*|https?:\/\/[^\s)>\]]+/i;
|
|
115
|
+
const URL_RE_GLOBAL = new RegExp(URL_RE.source, 'gi');
|
|
116
|
+
|
|
117
|
+
// A stranger cannot open localhost, loopback or *.local addresses, so they
|
|
118
|
+
// never count as the check link no matter which field they came from.
|
|
119
|
+
// Parsing with URL normalizes tricks like 127.1 or 0x7f.1 into 127.0.0.1.
|
|
120
|
+
function localCheckUrl(url) {
|
|
121
|
+
let host;
|
|
122
|
+
try {
|
|
123
|
+
host = new URL(url).hostname.toLowerCase();
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (host.startsWith('[') && host.endsWith(']')) host = host.slice(1, -1);
|
|
128
|
+
return host === 'localhost'
|
|
129
|
+
|| host === '0.0.0.0'
|
|
130
|
+
|| host === '::1'
|
|
131
|
+
|| host.startsWith('127.')
|
|
132
|
+
|| host.endsWith('.local');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A checked/tested note that is nothing but a local URL carries no check a
|
|
136
|
+
// stranger can run; the verifier line speaks instead.
|
|
137
|
+
function localOnlyUrl(text) {
|
|
138
|
+
const value = trimPunctuation(text);
|
|
139
|
+
return /^https?:\/\/\S+$/i.test(value) && localCheckUrl(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function firstUrl(...values) {
|
|
143
|
+
for (const value of values) {
|
|
144
|
+
if (!value) continue;
|
|
145
|
+
for (const raw of String(value).match(URL_RE_GLOBAL) || []) {
|
|
146
|
+
const url = raw.replace(/[.,;:]+$/, '');
|
|
147
|
+
if (!localCheckUrl(url)) return url;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The link a stranger can click. Read explicit link fields first, then any
|
|
154
|
+
// URL the landing text already mentions. Local-only hosts never count, and
|
|
155
|
+
// empty means the receipt has none.
|
|
156
|
+
function checkLink(receipt) {
|
|
157
|
+
const land = landing(receipt);
|
|
158
|
+
const res = result(receipt);
|
|
159
|
+
return firstUrl(
|
|
160
|
+
land.link, land.url, land.check_url, land.proof_url,
|
|
161
|
+
res.link, res.url, res.pr_url, res.share_url, res.tick?.pr_url, res.tick?.share_url,
|
|
162
|
+
land.checked, land.tested,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// What a reader can check: the link when there is one, else the plain
|
|
167
|
+
// check that passed. Never "verifier passed" or "receipt is present".
|
|
168
|
+
function checkText(receipt) {
|
|
169
|
+
const link = checkLink(receipt);
|
|
170
|
+
if (link) return link;
|
|
102
171
|
const land = landing(receipt);
|
|
103
172
|
const verifier = verifierResult(receipt);
|
|
104
|
-
if (land.checked) return directProofText(land.checked);
|
|
105
|
-
if (land.tested) return directProofText(land.tested);
|
|
173
|
+
if (land.checked && !localOnlyUrl(land.checked)) return directProofText(land.checked);
|
|
174
|
+
if (land.tested && !localOnlyUrl(land.tested)) return directProofText(land.tested);
|
|
106
175
|
if (verifier) {
|
|
107
|
-
const command = lowerLine(verifier.command || receipt?.verifier || '
|
|
176
|
+
const command = lowerLine(verifier.command || receipt?.verifier || 'the checks');
|
|
108
177
|
return `${command} ${verifier.passed ? 'passed' : 'failed'}`;
|
|
109
178
|
}
|
|
110
|
-
if (result(receipt).passed === true) return '
|
|
111
|
-
|
|
112
|
-
|
|
179
|
+
if (result(receipt).passed === true) return 'the checks passed';
|
|
180
|
+
return '';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Dollars a mission actually brought in. Read from the landing (or result)
|
|
184
|
+
// so a member can write it next to what happened; the backend sums the same
|
|
185
|
+
// field into the member rating. Only a real number or a plain decimal string
|
|
186
|
+
// may count: Number() alone would read true as 1, [5] as 5, "0x10" as 16 and
|
|
187
|
+
// "1e3" as 1000. Junk and negatives count as zero.
|
|
188
|
+
const PLAIN_DECIMAL_RE = /^\d+(\.\d+)?$/;
|
|
189
|
+
|
|
190
|
+
function plainDecimalAmount(value) {
|
|
191
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
|
|
192
|
+
if (typeof value !== 'string') return 0;
|
|
193
|
+
const text = value.trim();
|
|
194
|
+
return PLAIN_DECIMAL_RE.test(text) ? Number(text) : 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function earnedUsd(receipt) {
|
|
198
|
+
const land = landing(receipt);
|
|
199
|
+
const res = result(receipt);
|
|
200
|
+
for (const value of [land.earned_usd, res.earned_usd, res.tick?.earned_usd]) {
|
|
201
|
+
if (value === undefined || value === null || value === '') continue;
|
|
202
|
+
const amount = plainDecimalAmount(value);
|
|
203
|
+
return amount > 0 ? Math.round(amount * 100) / 100 : 0;
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function usdText(amount) {
|
|
209
|
+
const whole = Number.isInteger(amount);
|
|
210
|
+
return `$${amount.toLocaleString('en-US', { minimumFractionDigits: whole ? 0 : 2, maximumFractionDigits: 2 })}`;
|
|
113
211
|
}
|
|
114
212
|
|
|
115
213
|
function nextText(receipt) {
|
|
@@ -117,43 +215,53 @@ function nextText(receipt) {
|
|
|
117
215
|
return lowerLine(text);
|
|
118
216
|
}
|
|
119
217
|
|
|
218
|
+
function whatText(receipt, max = 180) {
|
|
219
|
+
return historicalLandingText(trimPunctuation(changedText(receipt)), max);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// One line: "What happened. Check it: <link or check>."
|
|
223
|
+
function checkLine(receipt, max = 160) {
|
|
224
|
+
const check = trimPunctuation(checkText(receipt));
|
|
225
|
+
if (!check) return '';
|
|
226
|
+
// A link stays bare so it copies clean; a plain check reads as a sentence.
|
|
227
|
+
return firstUrl(check) ? `Check it: ${check}` : `Check it: ${truncateLine(check, max)}.`;
|
|
228
|
+
}
|
|
229
|
+
|
|
120
230
|
function renderEmailLine(receipt) {
|
|
121
|
-
const what =
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
return `${what}; ${scale}; we know because ${proof}.`;
|
|
231
|
+
const what = sentence(whatText(receipt));
|
|
232
|
+
const check = checkLine(receipt);
|
|
233
|
+
return check ? `${what} ${check}` : what;
|
|
125
234
|
}
|
|
126
235
|
|
|
127
236
|
function renderMorningCardRow(receipt) {
|
|
128
|
-
return `-
|
|
237
|
+
return `- ${renderEmailLine(receipt).replace(/\.$/, '')}`;
|
|
129
238
|
}
|
|
130
239
|
|
|
240
|
+
// Three lines lead. The machinery (size, status, mission id) sits below as
|
|
241
|
+
// details, where a builder can find it and a customer never has to read it.
|
|
131
242
|
function renderPageSection(receipt) {
|
|
132
243
|
const next = nextText(receipt);
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (next) lines.push(`- next: ${truncateLine(trimPunctuation(next), 160)}`);
|
|
244
|
+
const check = checkLine(receipt);
|
|
245
|
+
const lines = ['## mission receipt', '', sentence(whatText(receipt, 160))];
|
|
246
|
+
if (check) lines.push(check);
|
|
247
|
+
if (next) lines.push(`Next: ${sentence(truncateLine(next, 160))}`);
|
|
248
|
+
const details = [scaleText(receipt), `status ${statusText(receipt)}`];
|
|
249
|
+
const earned = earnedUsd(receipt);
|
|
250
|
+
if (earned > 0) details.push(`earned ${usdText(earned)}`);
|
|
251
|
+
if (receipt?.mission_id) details.push(`mission ${lowerLine(receipt.mission_id)}`);
|
|
252
|
+
lines.push('', `details: ${details.join('; ')}`);
|
|
143
253
|
return lines.join('\n');
|
|
144
254
|
}
|
|
145
255
|
|
|
146
256
|
function renderCard(receipt) {
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const scale = scaleText(receipt);
|
|
150
|
-
const proof = truncateLine(trimPunctuation(proofText(receipt)), 88);
|
|
257
|
+
const what = sentence(whatText(receipt, 64));
|
|
258
|
+
const check = trimPunctuation(checkText(receipt));
|
|
151
259
|
return {
|
|
152
260
|
kind: 'statement',
|
|
153
261
|
headline: what,
|
|
154
262
|
text: what,
|
|
155
|
-
kicker:
|
|
156
|
-
sub:
|
|
263
|
+
kicker: 'mission receipt',
|
|
264
|
+
sub: check ? `Check it: ${truncateLine(check, 88)}` : '',
|
|
157
265
|
brand: 'atris',
|
|
158
266
|
size: 'og',
|
|
159
267
|
theme: 'atris',
|
|
@@ -161,6 +269,7 @@ function renderCard(receipt) {
|
|
|
161
269
|
}
|
|
162
270
|
|
|
163
271
|
module.exports = {
|
|
272
|
+
earnedUsd,
|
|
164
273
|
renderCard,
|
|
165
274
|
renderPageSection,
|
|
166
275
|
renderEmailLine,
|
package/lib/runner-command.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// so autonomous runs are reproducible, while keeping explicit per-run/env knobs
|
|
9
9
|
// first in precedence. Precedence: explicit model -> ATRIS_RUNNER_MODEL env ->
|
|
10
10
|
// ATRIS_RUNNER_PROFILE -> legacy ATRIS_CLAUDE_MODEL env -> pinned default.
|
|
11
|
-
const DEFAULT_CLAUDE_RUNNER_MODEL = 'claude-opus-
|
|
11
|
+
const DEFAULT_CLAUDE_RUNNER_MODEL = 'claude-opus-5-5';
|
|
12
12
|
const DEFAULT_FABLE_RUNNER_MODEL = 'claude-fable-5';
|
|
13
13
|
const DEFAULT_CLAUDE_RUNNER_BIN = 'claude';
|
|
14
14
|
|
package/lib/scorecard.js
CHANGED
|
@@ -245,14 +245,14 @@ function writeScorecard(atrisDir, data) {
|
|
|
245
245
|
|
|
246
246
|
// Ensure scorecards.md exists
|
|
247
247
|
if (!fs.existsSync(scorecardsPath)) {
|
|
248
|
-
const template = `# scorecards.md
|
|
248
|
+
const template = `# scorecards.md: Endgame Results\n\n> Append-only. One line per closed endgame. Records outcome metrics from the horizon.\n\n---\n\n`;
|
|
249
249
|
fs.writeFileSync(scorecardsPath, template, 'utf8');
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
// Format: - **[date] slug
|
|
252
|
+
// Format: - **[date] slug** - shipped: X/Y - wall-clock: Nh - halt: Z% - reward: total - lessons: N
|
|
253
253
|
const haltPercent = Math.round(haltRatio * 100);
|
|
254
254
|
const wallClockStr = wallClockHours < 1 ? `${Math.round(wallClockHours * 60)}m` : `${wallClockHours.toFixed(1)}h`;
|
|
255
|
-
const line = `- **[${endDate}] ${slug}**
|
|
255
|
+
const line = `- **[${endDate}] ${slug}** - shipped: ${tasksShipped}/${tasksAttempted} - wall-clock: ${wallClockStr} - halt: ${haltPercent}% - reward: ${totalReward} - lessons: ${lessonsGenerated}\n`;
|
|
256
256
|
|
|
257
257
|
// Append to file
|
|
258
258
|
fs.appendFileSync(scorecardsPath, line, 'utf8');
|
|
@@ -302,7 +302,7 @@ function readScorecards(atrisDir) {
|
|
|
302
302
|
const scorecards = [];
|
|
303
303
|
|
|
304
304
|
for (const line of content.split('\n')) {
|
|
305
|
-
const match = line.match(/^- \*\*\[(.+?)\]\s+(.+?)\*\*\s
|
|
305
|
+
const match = line.match(/^- \*\*\[(.+?)\]\s+(.+?)\*\*\s*[-\u2014]\s*shipped:\s*(\d+)\/(\d+)\s*[-\u2014]\s*wall-clock:\s*(.+?)\s*[-\u2014]\s*halt:\s*(\d+)%\s*[-\u2014]\s*reward:\s*(-?\d+)\s*[-\u2014]\s*lessons:\s*(\d+)$/);
|
|
306
306
|
if (!match) continue;
|
|
307
307
|
|
|
308
308
|
const [, endDate, slug, shipped, attempted, wallClockStr, haltPercent, reward, lessons] = match;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Block the current thread for ms milliseconds without burning a core.
|
|
4
|
+
// Atomics.wait parks the thread; a Date.now() loop keeps it at full CPU.
|
|
5
|
+
function sleepSync(ms) {
|
|
6
|
+
const duration = Math.max(0, Number(ms) || 0);
|
|
7
|
+
if (!duration) return;
|
|
8
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, duration);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { sleepSync };
|
package/lib/state-detection.js
CHANGED
|
@@ -271,6 +271,32 @@ function getTodayInboxItems(workspaceDir) {
|
|
|
271
271
|
/**
|
|
272
272
|
* Detect workspace state: fresh install, has inbox items, in-progress work, or blocked
|
|
273
273
|
*/
|
|
274
|
+
// Blocked means a task whose status is failed: the DB has no separate
|
|
275
|
+
// blocked status, doneTask logs a failed task as a "blocked" event, and the
|
|
276
|
+
// board renders failed tasks under "## Blocked". Once this workspace has rows
|
|
277
|
+
// in the task DB, the DB is the only source. The DB file is shared by every
|
|
278
|
+
// workspace on the machine, so a workspace with no rows there falls back to
|
|
279
|
+
// the Blocked section of its own TODO.md, never a word elsewhere in a title.
|
|
280
|
+
function hasBlockedTasks(atrisDir, todoFile, legacyTaskContextFile) {
|
|
281
|
+
try {
|
|
282
|
+
const taskDb = require('./task-db');
|
|
283
|
+
if (fs.existsSync(taskDb.getDbPath())) {
|
|
284
|
+
const db = taskDb.open();
|
|
285
|
+
const workspaceRoot = taskDb.workspaceRoot(path.dirname(path.resolve(atrisDir)));
|
|
286
|
+
if (taskDb.listTasks(db, { workspaceRoot, limit: 1 }).length) {
|
|
287
|
+
return taskDb.listTasks(db, { workspaceRoot, status: 'failed', limit: 1 }).length > 0;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (e) {
|
|
291
|
+
if (process.env.ATRIS_DEBUG) console.error('[state-detection] db blocked read failed:', e.message);
|
|
292
|
+
}
|
|
293
|
+
const taskFilePath = fs.existsSync(todoFile)
|
|
294
|
+
? todoFile
|
|
295
|
+
: (fs.existsSync(legacyTaskContextFile) ? legacyTaskContextFile : null);
|
|
296
|
+
if (!taskFilePath) return false;
|
|
297
|
+
return getTasksFromTodoSection(fs.readFileSync(taskFilePath, 'utf8'), 'Blocked').length > 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
274
300
|
function detectWorkspaceState(workspaceDir) {
|
|
275
301
|
const atrisDir = path.join(workspaceDir, 'atris');
|
|
276
302
|
const featuresDir = path.join(atrisDir, 'features');
|
|
@@ -317,15 +343,8 @@ function detectWorkspaceState(workspaceDir) {
|
|
|
317
343
|
}
|
|
318
344
|
}
|
|
319
345
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
? todoFile
|
|
323
|
-
: (fs.existsSync(legacyTaskContextFile) ? legacyTaskContextFile : null);
|
|
324
|
-
if (taskFilePath) {
|
|
325
|
-
const content = fs.readFileSync(taskFilePath, 'utf8');
|
|
326
|
-
if (content.includes('Risk Level: High') || /\bblocked\b/i.test(content)) {
|
|
327
|
-
return { state: 'blocked', reason: 'Tasks marked as blocked or high risk' };
|
|
328
|
-
}
|
|
346
|
+
if (hasBlockedTasks(atrisDir, todoFile, legacyTaskContextFile)) {
|
|
347
|
+
return { state: 'blocked', reason: 'Tasks marked as blocked' };
|
|
329
348
|
}
|
|
330
349
|
|
|
331
350
|
// Default: setup complete but no active work
|
package/lib/task-db.js
CHANGED
|
@@ -34,6 +34,7 @@ const { isDecisionTask } = require('./task-decision');
|
|
|
34
34
|
const { parseVerifyCommand } = require('./auto-accept-certified');
|
|
35
35
|
const { taskExplanation } = require('./task-explanation');
|
|
36
36
|
const { treeHashFor } = require('./tree-hash');
|
|
37
|
+
const { sleepSync } = require('./sleep-sync');
|
|
37
38
|
|
|
38
39
|
const DEFAULT_DB_PATH = path.join(os.homedir(), '.atris', 'tasks.db');
|
|
39
40
|
const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
@@ -2224,8 +2225,7 @@ function withBusyRetry(fn, attempts = 8) {
|
|
|
2224
2225
|
const busy = /SQLITE_BUSY|database is locked/i.test(msg) || code === 'SQLITE_BUSY' || code === 5;
|
|
2225
2226
|
if (!busy) throw e;
|
|
2226
2227
|
// Sleep synchronously, node:sqlite is sync; matches the rest of the API
|
|
2227
|
-
|
|
2228
|
-
while (Date.now() < end) {} // tight loop is fine, delay is small
|
|
2228
|
+
sleepSync(delay + Math.floor(Math.random() * delay));
|
|
2229
2229
|
delay = Math.min(delay * 2, 500);
|
|
2230
2230
|
}
|
|
2231
2231
|
}
|
|
@@ -2234,6 +2234,7 @@ function withBusyRetry(fn, attempts = 8) {
|
|
|
2234
2234
|
|
|
2235
2235
|
module.exports = {
|
|
2236
2236
|
open,
|
|
2237
|
+
withBusyRetry,
|
|
2237
2238
|
close,
|
|
2238
2239
|
getDbPath,
|
|
2239
2240
|
workspaceRoot,
|
package/lib/todo-fallback.js
CHANGED
|
@@ -34,7 +34,7 @@ function cleanTaskTitle(text) {
|
|
|
34
34
|
const withoutTags = raw.replace(/\s*\[[\w-]+\]/g, '').trim();
|
|
35
35
|
const bold = withoutTags.match(/^\*\*(.+?)\*\*\s*(?:[\u2014-]\s*)?(.*)$/);
|
|
36
36
|
if (!bold) return withoutTags;
|
|
37
|
-
return [bold[1], bold[2]].filter(Boolean).join(' \
|
|
37
|
+
return [bold[1], bold[2]].filter(Boolean).join(' - ').replace(/\s*\u2014\s*/g, ' - ').trim();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function parseSection(content, sectionName) {
|
package/lib/wish-delegate.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { BACKEND_ROOT_HINT, resolveBackendRoot } = require('../utils/backend-root');
|
|
5
6
|
const { getLogPath, ensureLogDirectory, createLogFile, addInboxIdea } = require('./file-ops');
|
|
6
7
|
const { resolveFunctionalOwner } = require('./functional-owner');
|
|
7
8
|
const { buildMissionRoom, writeMissionRoomReceipt } = require('./mission-room');
|
|
@@ -55,7 +56,6 @@ const {
|
|
|
55
56
|
} = require('./wish-audit');
|
|
56
57
|
|
|
57
58
|
const METRIC_OPS = { '>=': '--gte', '<=': '--lte', '==': '--eq', '>': '--gt', '<': '--lt' };
|
|
58
|
-
const METRIC_VERIFY_ROOT = '/Users/keshavrao/arena/atrisos-backend';
|
|
59
59
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
60
60
|
const HOUR_MS = 60 * 60 * 1000;
|
|
61
61
|
const IMPLICIT_ANSWER_STALE_MS = HOUR_MS;
|
|
@@ -105,8 +105,11 @@ function parseMetricExpression(text) {
|
|
|
105
105
|
|
|
106
106
|
// Wire to backend/scripts/metric_verify.py: exits 0 when the metric meets
|
|
107
107
|
// target and prints a JSON line with value + proof_uri.
|
|
108
|
-
function metricVerifierCommand(parsed) {
|
|
109
|
-
|
|
108
|
+
function metricVerifierCommand(parsed, workspace) {
|
|
109
|
+
const backend = resolveBackendRoot(workspace);
|
|
110
|
+
if (!backend) throw new Error(BACKEND_ROOT_HINT);
|
|
111
|
+
const quoted = `'${backend.replace(/'/g, `'\\''`)}'`;
|
|
112
|
+
return `cd ${quoted} && venv/bin/python backend/scripts/metric_verify.py ${parsed.metric} ${parsed.flag} ${parsed.target}`;
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
const MISSION_TERMINAL_STATUSES = new Set(['complete', 'stopped']);
|
|
@@ -489,7 +492,9 @@ function scorecardText(value) {
|
|
|
489
492
|
}
|
|
490
493
|
|
|
491
494
|
function readWishReviewRewardRows(root = process.cwd()) {
|
|
492
|
-
const
|
|
495
|
+
const backend = resolveBackendRoot(root);
|
|
496
|
+
if (!backend) return [];
|
|
497
|
+
const file = path.join(backend, '.atris', 'state', 'scorecards.jsonl');
|
|
493
498
|
return readJsonLines(file).filter((row) => {
|
|
494
499
|
const haystack = `${scorecardText(row.source)} ${scorecardText(row.feedback)}`;
|
|
495
500
|
return /wish_review/i.test(haystack);
|
|
@@ -514,6 +519,7 @@ function reviewLine(review) {
|
|
|
514
519
|
|
|
515
520
|
function printRewards(root = process.cwd()) {
|
|
516
521
|
const reviews = readWishEvents(root).filter((event) => event && event.kind === 'review');
|
|
522
|
+
if (!resolveBackendRoot(root)) console.error(BACKEND_ROOT_HINT);
|
|
517
523
|
const rewardRows = readWishReviewRewardRows(root);
|
|
518
524
|
console.log(`reviews count: ${reviews.length}`);
|
|
519
525
|
console.log(`avg score: ${averageReviewScore(reviews)}`);
|
|
@@ -749,7 +755,7 @@ function startWishDelegation(wish, audit, root, options = {}) {
|
|
|
749
755
|
const suppliedVerifyHasCommand = !!(suppliedVerifyPlan && suppliedVerifyPlan.command);
|
|
750
756
|
const userVerifyProvided = options.userVerifyProvided === true || options.verifyPlanExplicit === true;
|
|
751
757
|
let verifyPlan = metricPlan
|
|
752
|
-
? { command: metricVerifierCommand(metricPlan), outcome: `metric ${metricPlan.expression} is met`, status: 'metric' }
|
|
758
|
+
? { command: metricVerifierCommand(metricPlan, root), outcome: `metric ${metricPlan.expression} is met`, status: 'metric' }
|
|
753
759
|
: (suppliedVerifyPlan || deriveVerifyPlan(taskText));
|
|
754
760
|
if (!metricPlan && frontendWish && designContext.auditCommand && !userVerifyProvided && !suppliedVerifyHasCommand && !(verifyPlan && verifyPlan.command)) {
|
|
755
761
|
verifyPlan = designVerifyPlan(designContext);
|
|
@@ -1529,6 +1535,10 @@ function runCapturedWish(text, root = process.cwd(), options = {}) {
|
|
|
1529
1535
|
const asJson = !!options.asJson;
|
|
1530
1536
|
const noMission = !!options.noMission;
|
|
1531
1537
|
const builderMode = String(options.asMode || '').trim() === 'builder';
|
|
1538
|
+
if (options.metric?.ok && !resolveBackendRoot(root)) {
|
|
1539
|
+
console.error(BACKEND_ROOT_HINT);
|
|
1540
|
+
return 2;
|
|
1541
|
+
}
|
|
1532
1542
|
let engineOverride = '';
|
|
1533
1543
|
try {
|
|
1534
1544
|
engineOverride = validateEngineOverride(options.engineOverride, root);
|
|
@@ -1617,6 +1627,10 @@ function runAgainWish(positionals, root = process.cwd(), options = {}) {
|
|
|
1617
1627
|
function applyAnswer(wish, answer, root, options = {}) {
|
|
1618
1628
|
const asJson = !!options.asJson;
|
|
1619
1629
|
const noMission = !!options.noMission;
|
|
1630
|
+
if (wish.metric && !resolveBackendRoot(root)) {
|
|
1631
|
+
console.error(BACKEND_ROOT_HINT);
|
|
1632
|
+
return 2;
|
|
1633
|
+
}
|
|
1620
1634
|
let engineOverride = '';
|
|
1621
1635
|
try {
|
|
1622
1636
|
engineOverride = validateEngineOverride(options.engineOverride, root);
|
package/mcp/atris-mcp/index.mjs
CHANGED
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
// Auth resolves the same way as the CLI: ATRIS_API_KEY, then
|
|
5
5
|
// the logged-in atris token, then ~/.atris/design-api-key.
|
|
6
6
|
|
|
7
|
-
import { realpathSync } from 'node:fs';
|
|
7
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
10
|
import designApi from '../../lib/design-api.js';
|
|
13
11
|
|
|
14
12
|
const { resolveDesignKey, designRequest, pollDesignJob, billingOf } = designApi;
|
|
@@ -141,34 +139,89 @@ export async function handleTool(name, args = {}, key) {
|
|
|
141
139
|
return fail(`unknown tool: ${name}`);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
142
|
+
async function serveStdio() {
|
|
143
|
+
const { name, version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
144
|
+
const knownVersions = new Set(['2024-11-05', '2025-03-26', '2025-06-18', '2025-11-25']);
|
|
145
|
+
const send = (message, onWritten) => process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`, onWritten);
|
|
146
|
+
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
147
|
+
const inFlight = new Set();
|
|
148
|
+
const pendingById = new Map();
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const key = resolveDesignKey();
|
|
154
|
-
if (!key) {
|
|
155
|
-
return fail('no api key found. set ATRIS_API_KEY or run: atris login (or save a key in ~/.atris/design-api-key)');
|
|
156
|
-
}
|
|
150
|
+
for await (const line of input) {
|
|
151
|
+
if (!line.trim()) continue;
|
|
152
|
+
let request;
|
|
157
153
|
try {
|
|
158
|
-
|
|
159
|
-
} catch
|
|
160
|
-
|
|
154
|
+
request = JSON.parse(line);
|
|
155
|
+
} catch {
|
|
156
|
+
send({ id: null, error: { code: -32700, message: 'parse error' } });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!request || typeof request !== 'object' || Array.isArray(request)
|
|
160
|
+
|| request.jsonrpc !== '2.0' || typeof request.method !== 'string') {
|
|
161
|
+
send({ id: request?.id ?? null, error: { code: -32600, message: 'invalid request' } });
|
|
162
|
+
continue;
|
|
161
163
|
}
|
|
162
|
-
|
|
164
|
+
if (request.method === 'notifications/cancelled') {
|
|
165
|
+
const pending = pendingById.get(request.params?.requestId);
|
|
166
|
+
if (pending) pending.cancelled = true;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!Object.hasOwn(request, 'id')) continue;
|
|
163
170
|
|
|
164
|
-
|
|
171
|
+
const id = request.id;
|
|
172
|
+
let result;
|
|
173
|
+
if (request.method === 'initialize') {
|
|
174
|
+
result = {
|
|
175
|
+
protocolVersion: knownVersions.has(request.params?.protocolVersion)
|
|
176
|
+
? request.params.protocolVersion : '2025-06-18',
|
|
177
|
+
capabilities: { tools: {} },
|
|
178
|
+
serverInfo: { name, version },
|
|
179
|
+
};
|
|
180
|
+
} else if (request.method === 'ping') {
|
|
181
|
+
result = {};
|
|
182
|
+
} else if (request.method === 'tools/list') {
|
|
183
|
+
result = { tools: TOOLS };
|
|
184
|
+
} else if (request.method === 'tools/call') {
|
|
185
|
+
const pending = { cancelled: false };
|
|
186
|
+
const work = (async () => {
|
|
187
|
+
try {
|
|
188
|
+
const key = resolveDesignKey();
|
|
189
|
+
result = key
|
|
190
|
+
? await handleTool(request.params?.name, request.params?.arguments || {}, key)
|
|
191
|
+
: fail('no api key found. set ATRIS_API_KEY or run: atris login (or save a key in ~/.atris/design-api-key)');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
result = fail(`design call failed: ${(error && error.message) || error}`);
|
|
194
|
+
}
|
|
195
|
+
if (!pending.cancelled) {
|
|
196
|
+
await new Promise((resolve, reject) => {
|
|
197
|
+
send({ id, result }, (error) => (error ? reject(error) : resolve()));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
inFlight.add(work);
|
|
202
|
+
pendingById.set(id, pending);
|
|
203
|
+
const done = () => {
|
|
204
|
+
inFlight.delete(work);
|
|
205
|
+
if (pendingById.get(id) === pending) pendingById.delete(id);
|
|
206
|
+
};
|
|
207
|
+
work.then(done, (error) => {
|
|
208
|
+
done();
|
|
209
|
+
process.stderr.write(`mcp response write failed: ${(error && error.message) || error}\n`);
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
} else {
|
|
214
|
+
send({ id, error: { code: -32601, message: 'method not found' } });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
send({ id, result });
|
|
218
|
+
}
|
|
219
|
+
await Promise.allSettled(inFlight);
|
|
165
220
|
}
|
|
166
221
|
|
|
167
222
|
// Auto-start only when run directly (`atris mcp`, `npx atris-mcp`,
|
|
168
223
|
// node index.mjs); realpathSync resolves the npm bin symlink.
|
|
169
224
|
const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : '';
|
|
170
225
|
if (invokedPath && invokedPath === fileURLToPath(import.meta.url)) {
|
|
171
|
-
|
|
172
|
-
const transport = new StdioServerTransport();
|
|
173
|
-
await server.connect(transport);
|
|
226
|
+
await serveStdio();
|
|
174
227
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.58.
|
|
3
|
+
"version": "3.58.8",
|
|
4
4
|
"description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
|
|
5
5
|
"main": "bin/atris.js",
|
|
6
6
|
"bin": {
|
|
@@ -101,8 +101,5 @@
|
|
|
101
101
|
"bugs": {
|
|
102
102
|
"url": "https://github.com/atrislabs/atris/issues"
|
|
103
103
|
},
|
|
104
|
-
"homepage": "https://github.com/atrislabs/atris#readme"
|
|
105
|
-
"dependencies": {
|
|
106
|
-
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
107
|
-
}
|
|
104
|
+
"homepage": "https://github.com/atrislabs/atris#readme"
|
|
108
105
|
}
|
package/scripts/det/det.js
CHANGED
|
@@ -84,7 +84,7 @@ function catalogText() {
|
|
|
84
84
|
);
|
|
85
85
|
const gitRows = GIT_SCRIPTS.map((g) => ` ${g.name.padEnd(15)} ${g.usage}\n ${g.ask}`);
|
|
86
86
|
return (
|
|
87
|
-
'deterministic task scripts
|
|
87
|
+
'deterministic task scripts: run node det.js <script> <mode> < input\n\n' +
|
|
88
88
|
rows.join('\n\n') +
|
|
89
89
|
'\n\ngit-facing (read the repo, run the script directly; not via det.js):\n\n' +
|
|
90
90
|
gitRows.join('\n\n') +
|
|
@@ -121,7 +121,7 @@ async function main() {
|
|
|
121
121
|
}
|
|
122
122
|
const git = GIT_SCRIPTS.find((g) => g.name === script);
|
|
123
123
|
if (git) {
|
|
124
|
-
process.stdout.write(`${git.name} reads the repo, not stdin
|
|
124
|
+
process.stdout.write(`${git.name} reads the repo, not stdin; run it directly:\n ${git.usage}\n`);
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
const entry = CATALOG[script];
|
|
@@ -78,7 +78,7 @@ function areaBullets(files) {
|
|
|
78
78
|
if (counts.A) parts.push(`${counts.A} added`);
|
|
79
79
|
if (counts.M) parts.push(`${counts.M} changed`);
|
|
80
80
|
if (counts.D) parts.push(`${counts.D} removed`);
|
|
81
|
-
bullets.push(`- **${area}
|
|
81
|
+
bullets.push(`- **${area}**: ${parts.join(', ')} (+${added}/-${deleted})`);
|
|
82
82
|
}
|
|
83
83
|
return bullets;
|
|
84
84
|
}
|
|
@@ -108,7 +108,7 @@ function build(input) {
|
|
|
108
108
|
const commits = (input && input.commits) || [];
|
|
109
109
|
const files = (input && input.files) || [];
|
|
110
110
|
if (!commits.length && !files.length) {
|
|
111
|
-
return { error: 'no commits or files vs base
|
|
111
|
+
return { error: 'no commits or files vs base: is the branch ahead of it?' };
|
|
112
112
|
}
|
|
113
113
|
const totals = files.reduce(
|
|
114
114
|
(a, f) => ({ added: a.added + (f.added || 0), deleted: a.deleted + (f.deleted || 0) }),
|
package/scripts/det/test.js
CHANGED
|
@@ -211,7 +211,7 @@ check(
|
|
|
211
211
|
{ path: 'scripts/det/b.js', status: 'M', added: 2, deleted: 1 },
|
|
212
212
|
{ path: 'test/x.test.js', status: 'A', added: 5, deleted: 0 },
|
|
213
213
|
]),
|
|
214
|
-
['- **scripts
|
|
214
|
+
['- **scripts**: 1 added, 1 changed (+12/-1)', '- **test**: 1 added (+5/-0)']
|
|
215
215
|
);
|
|
216
216
|
// test plan lists touched test files, then a check per non-test area
|
|
217
217
|
check(
|
|
@@ -267,7 +267,7 @@ check('det.git.json', JSON.parse(catalogJson()).git['commit-msg'].usage.includes
|
|
|
267
267
|
// stdin catalog stays separate from the git list (routing must not mix them)
|
|
268
268
|
check('det.git.notRoutable', CATALOG['commit-msg'], undefined);
|
|
269
269
|
|
|
270
|
-
console.log(`ok
|
|
270
|
+
console.log(`ok: ${passed} checks passed`);
|
|
271
271
|
|
|
272
272
|
// hunk-filter: keeps only matching hunks, drops non-matching files entirely
|
|
273
273
|
{
|
package/utils/api.js
CHANGED
|
@@ -97,7 +97,7 @@ function httpRequest(urlString, options) {
|
|
|
97
97
|
// Socket idle timeout (fires if no data received for this duration)
|
|
98
98
|
if (timeoutMs > 0) {
|
|
99
99
|
req.setTimeout(timeoutMs, () => {
|
|
100
|
-
req.destroy(new Error(`
|
|
100
|
+
req.destroy(new Error(`request timeout after ${Math.round(timeoutMs / 1000)}s, try --timeout=300`));
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
// Hard deadline — kill request after 2x the timeout regardless of activity
|