atris 3.72.4 → 3.72.5
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 +1 -1
- package/commands/gm.js +1 -1
- package/commands/task.js +55 -18
- package/lib/gm-phone.js +16 -6
- package/lib/site.js +9 -1
- package/lib/zen-apply.js +5 -1
- package/package.json +1 -1
- package/scripts/det/xsearch-bench.js +1 -0
package/README.md
CHANGED
|
@@ -367,7 +367,7 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
|
|
|
367
367
|
- `atris activate` loads the current wiki status so the next session starts with project memory, not just tasks
|
|
368
368
|
- `atris member` keeps team-member identity and learning local-first: `MEMBER.md` is the role contract, `goals.json` is the machine-readable goal/experiment state, `goals.md` is the human readout, and `logs/YYYY-MM-DD.md` records what happened. Use `atris member goal`, `tick`, `status`, `block`, and `review <name> <experiment-id> --accept --proof "..." --value 1..5` to test whether a member is making useful progress or needs the operator/orchestrator.
|
|
369
369
|
- `atris codex-goal` is the read-only bridge for native Codex `/goal`: `status` inspects the live goal database, while the legacy `reset` command now refuses every mutation. Active tasks continue in place; completed tasks retain their final goal, and new work or recurring monitors use a new dedicated Codex task.
|
|
370
|
-
- `atris task` keeps durable local task state and append-only events for agents; `atris/TODO.md` is just a regenerated readable board. Run the loop with `atris task new`, `delegate "..." --to <owner>`, `next`, `say`, and `ready <id> --proof "..."`; human approval is `atris task accept <id>` (moves to Done, awards Career XP) or `revise <id> --note "..."`. Final task transitions also append the general daily log and, when a real `atris/team/<member>/MEMBER.md` matches the task owner, that member's daily log. Add `--json` for headless agents, `atris task serve` for the local board, and `atris task show <ref>` / `events --all` for the full ledger. Commands accept semantic refs (`OBL-18`), full IDs, or any unique prefix. In cloud business workspaces, Supabase `tasks` is the source of truth and Swarlo the live claim layer.
|
|
370
|
+
- `atris task` keeps durable local task state and append-only events for agents; `atris/TODO.md` is just a regenerated readable board. Run the loop with `atris task new`, `delegate "..." --to <owner>`, `next`, `say`, and `ready <id> --proof "..."`; human approval is `atris task accept <id>` (moves to Done, awards Career XP) or `revise <id> --note "..."`. Final task transitions also append the general daily log and, when a real `atris/team/<member>/MEMBER.md` matches the task owner, that member's daily log. Add `--json` for headless agents, `atris task serve` for the local board, and `atris task show <ref>` / `events --all` for the full ledger. The board prints a private link with a fresh token on each run and listens only on 127.0.0.1. Open that full link in the browser; API clients send its token in `x-atris-token` and use `application/json` for changes. Browser changes must come from the board's own origin. Commands accept semantic refs (`OBL-18`), full IDs, or any unique prefix. In cloud business workspaces, Supabase `tasks` is the source of truth and Swarlo the live claim layer.
|
|
371
371
|
- `atris mission` is the durable autonomy layer: start with an owner, verifier, runner, and stop condition; record bounded work with `mission tick`; close with `mission complete` only after proof. Runners include `manual`, `claude`, `atris2`, and `codex_goal`.
|
|
372
372
|
- `atris pulse` is the OS-cron heartbeat for overnight self-improvement. Use `atris pulse status`, `tick`, `run`, `install --cadence "<cron>" --days 7 --verify "npm test" --model claude-opus-5-5`, and `uninstall`.
|
|
373
373
|
- `atris experiments` runs small test packs in `atris/experiments/`
|
package/commands/gm.js
CHANGED
|
@@ -1525,7 +1525,7 @@ async function gmPhone(args = []) {
|
|
|
1525
1525
|
const opened = [];
|
|
1526
1526
|
try {
|
|
1527
1527
|
for (const address of addresses) {
|
|
1528
|
-
const server = http.createServer((req, res) => { handler(req, res); });
|
|
1528
|
+
const server = http.createServer((req, res) => { handler(req, res).catch(() => res.destroy()); });
|
|
1529
1529
|
await new Promise((resolve, reject) => {
|
|
1530
1530
|
server.once('error', reject);
|
|
1531
1531
|
server.listen(port, address, resolve);
|
package/commands/task.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const http = require('http');
|
|
8
|
+
const crypto = require('crypto');
|
|
8
9
|
const { execSync } = require('node:child_process');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const os = require('os');
|
|
@@ -12482,7 +12483,7 @@ function taskBoardTemplate(model) {
|
|
|
12482
12483
|
async function api(path, options = {}) {
|
|
12483
12484
|
const res = await fetch(path, {
|
|
12484
12485
|
...options,
|
|
12485
|
-
headers: { 'content-type': 'application/json', ...(options.headers || {}) }
|
|
12486
|
+
headers: { 'content-type': 'application/json', 'x-atris-token': new URLSearchParams(location.search).get('t') || '', ...(options.headers || {}) }
|
|
12486
12487
|
});
|
|
12487
12488
|
const data = await res.json();
|
|
12488
12489
|
if (!res.ok || data.ok === false) throw new Error(data.detail || data.reason || 'request failed');
|
|
@@ -12798,18 +12799,30 @@ function taskBoardHtml() {
|
|
|
12798
12799
|
}
|
|
12799
12800
|
|
|
12800
12801
|
function readJsonBody(req) {
|
|
12802
|
+
if (String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json') {
|
|
12803
|
+
return Promise.reject(Object.assign(new Error('json required'), { statusCode: 415 }));
|
|
12804
|
+
}
|
|
12801
12805
|
return new Promise((resolve, reject) => {
|
|
12802
|
-
let
|
|
12806
|
+
let size = 0;
|
|
12807
|
+
const chunks = [];
|
|
12803
12808
|
req.on('data', chunk => {
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12809
|
+
if (size > 1_000_000) return;
|
|
12810
|
+
size += chunk.length;
|
|
12811
|
+
if (size > 1_000_000) {
|
|
12812
|
+
reject(Object.assign(new Error('body too large'), { statusCode: 413 }));
|
|
12813
|
+
return;
|
|
12808
12814
|
}
|
|
12815
|
+
chunks.push(chunk);
|
|
12809
12816
|
});
|
|
12810
12817
|
req.on('end', () => {
|
|
12818
|
+
if (size > 1_000_000) return;
|
|
12819
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
12811
12820
|
if (!body.trim()) return resolve({});
|
|
12812
|
-
try {
|
|
12821
|
+
try {
|
|
12822
|
+
const value = JSON.parse(body);
|
|
12823
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('object required');
|
|
12824
|
+
resolve(value);
|
|
12825
|
+
} catch (e) { reject(Object.assign(e, { statusCode: 400 })); }
|
|
12813
12826
|
});
|
|
12814
12827
|
req.on('error', reject);
|
|
12815
12828
|
});
|
|
@@ -12818,14 +12831,16 @@ function readJsonBody(req) {
|
|
|
12818
12831
|
function sendJson(res, status, value, headers = {}) {
|
|
12819
12832
|
res.writeHead(status, {
|
|
12820
12833
|
'content-type': 'application/json; charset=utf-8',
|
|
12821
|
-
'
|
|
12834
|
+
'cache-control': 'no-store',
|
|
12835
|
+
'referrer-policy': 'no-referrer',
|
|
12836
|
+
'x-content-type-options': 'nosniff',
|
|
12822
12837
|
...headers,
|
|
12823
12838
|
});
|
|
12824
12839
|
res.end(JSON.stringify(value, null, 2));
|
|
12825
12840
|
}
|
|
12826
12841
|
|
|
12827
12842
|
function sendHtml(res, value) {
|
|
12828
|
-
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
12843
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'referrer-policy': 'no-referrer', 'x-content-type-options': 'nosniff' });
|
|
12829
12844
|
res.end(value);
|
|
12830
12845
|
}
|
|
12831
12846
|
|
|
@@ -13469,8 +13484,24 @@ function matchTaskApiRoute(req, url) {
|
|
|
13469
13484
|
return null;
|
|
13470
13485
|
}
|
|
13471
13486
|
|
|
13472
|
-
async function handleTaskApi(req, res, taskDb, db) {
|
|
13473
|
-
|
|
13487
|
+
async function handleTaskApi(req, res, taskDb, db, token) {
|
|
13488
|
+
let url;
|
|
13489
|
+
try {
|
|
13490
|
+
const target = req.url || '/';
|
|
13491
|
+
url = new URL(target, 'http://127.0.0.1');
|
|
13492
|
+
if (!target.startsWith('/') || target.startsWith('//') || /[\\#\0]/.test(target) || decodeURIComponent(url.pathname).includes('\0')) throw new Error('url');
|
|
13493
|
+
decodeURIComponent(target);
|
|
13494
|
+
} catch {
|
|
13495
|
+
return sendJson(res, 400, { ok: false, reason: 'bad_url' });
|
|
13496
|
+
}
|
|
13497
|
+
const host = String(req.headers.host || '').toLowerCase();
|
|
13498
|
+
const port = req.socket.localPort;
|
|
13499
|
+
if (![`127.0.0.1:${port}`, `localhost:${port}`].includes(host)) return sendJson(res, 403, { ok: false, reason: 'host' });
|
|
13500
|
+
const provided = Buffer.from(String(req.headers['x-atris-token'] || (req.method === 'GET' && url.searchParams.get('t')) || ''));
|
|
13501
|
+
const expected = Buffer.from(token);
|
|
13502
|
+
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) return sendJson(res, 403, { ok: false, reason: 'token' });
|
|
13503
|
+
if (req.method === 'GET' && (req.headers['transfer-encoding'] || Number(req.headers['content-length'] || 0) > 0)) return sendJson(res, 400, { ok: false, reason: 'body' });
|
|
13504
|
+
if (req.method !== 'GET' && req.headers.origin !== undefined && req.headers.origin !== `http://${host}`) return sendJson(res, 403, { ok: false, reason: 'origin' });
|
|
13474
13505
|
const matchedRoute = matchTaskApiRoute(req, url);
|
|
13475
13506
|
if (!matchedRoute) return sendJson(res, 404, { ok: false, reason: 'not_found' });
|
|
13476
13507
|
return matchedRoute.route.serve({
|
|
@@ -13486,26 +13517,32 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
13486
13517
|
function createTaskApiServer(taskApi = {}) {
|
|
13487
13518
|
const taskDb = taskApi.taskDb || getTaskDb();
|
|
13488
13519
|
const db = taskApi.db || taskDb.open();
|
|
13489
|
-
|
|
13490
|
-
|
|
13491
|
-
|
|
13492
|
-
|
|
13520
|
+
const token = crypto.randomBytes(32).toString('base64url');
|
|
13521
|
+
const server = http.createServer((req, res) => {
|
|
13522
|
+
handleTaskApi(req, res, taskDb, db, token).catch((error) => {
|
|
13523
|
+
if (res.headersSent || res.destroyed) return res.destroy();
|
|
13524
|
+
const status = error.statusCode || 500;
|
|
13525
|
+
sendJson(res, status, { ok: false, reason: status < 500 ? 'bad_request' : 'server_error' });
|
|
13526
|
+
}).catch(() => res.destroy());
|
|
13493
13527
|
});
|
|
13528
|
+
server.taskToken = token;
|
|
13529
|
+
return server;
|
|
13494
13530
|
}
|
|
13495
13531
|
|
|
13496
13532
|
function cmdServe(args) {
|
|
13497
13533
|
const host = String(flag(args, '--host') || '127.0.0.1');
|
|
13534
|
+
if (!['127.0.0.1', 'localhost'].includes(host)) throw new Error('task board only listens on 127.0.0.1');
|
|
13498
13535
|
const port = Number(flag(args, '--port') || process.env.PORT || 8787);
|
|
13499
13536
|
const taskDb = getTaskDb();
|
|
13500
13537
|
const db = taskDb.open();
|
|
13501
13538
|
const server = createTaskApiServer({ taskDb, db });
|
|
13502
13539
|
return new Promise((resolve, reject) => {
|
|
13503
13540
|
server.on('error', reject);
|
|
13504
|
-
server.listen(port,
|
|
13541
|
+
server.listen(port, '127.0.0.1', () => {
|
|
13505
13542
|
const addr = server.address();
|
|
13506
13543
|
const actualPort = addr && addr.port || port;
|
|
13507
|
-
console.log(`
|
|
13508
|
-
console.log(`
|
|
13544
|
+
console.log(`task board: http://${host}:${actualPort}/?t=${server.taskToken}`);
|
|
13545
|
+
console.log(`workspace: ${taskDb.workspaceRoot()}`);
|
|
13509
13546
|
});
|
|
13510
13547
|
|
|
13511
13548
|
const shutdown = () => {
|
package/lib/gm-phone.js
CHANGED
|
@@ -303,11 +303,20 @@ function tokenMatches(given, token) {
|
|
|
303
303
|
// look-alike hostname pointed at the Mac gets nothing.
|
|
304
304
|
function checkRequest(req, { token, hosts = [] }) {
|
|
305
305
|
const host = String(req.headers.host || '').toLowerCase();
|
|
306
|
-
|
|
306
|
+
let url;
|
|
307
|
+
try {
|
|
308
|
+
const target = req.url || '/';
|
|
309
|
+
url = new URL(target, 'http://local');
|
|
310
|
+
if (!target.startsWith('/') || target.startsWith('//') || /[\\#\0]/.test(target) || decodeURIComponent(url.pathname).includes('\0')) throw new Error('url');
|
|
311
|
+
decodeURIComponent(target);
|
|
312
|
+
} catch {
|
|
313
|
+
return { ok: false, status: 400, reason: 'url' };
|
|
314
|
+
}
|
|
307
315
|
if (!hosts.includes(host)) return { ok: false, status: 403, reason: 'host' };
|
|
308
316
|
const header = req.headers['x-gm-token'];
|
|
309
317
|
if (req.method === 'GET') {
|
|
310
318
|
if (!tokenMatches(url.searchParams.get('t') || header, token)) return { ok: false, status: 403, reason: 'token' };
|
|
319
|
+
if (req.headers['transfer-encoding'] || Number(req.headers['content-length'] || 0) > 0) return { ok: false, status: 400, reason: 'body' };
|
|
311
320
|
return { ok: true, path: url.pathname };
|
|
312
321
|
}
|
|
313
322
|
if (req.method === 'POST') {
|
|
@@ -324,10 +333,10 @@ function readBody(req) {
|
|
|
324
333
|
let size = 0;
|
|
325
334
|
const chunks = [];
|
|
326
335
|
req.on('data', (chunk) => {
|
|
336
|
+
if (size > BODY_MAX) return;
|
|
327
337
|
size += chunk.length;
|
|
328
338
|
if (size > BODY_MAX) {
|
|
329
339
|
reject(new Error('too_large'));
|
|
330
|
-
req.destroy();
|
|
331
340
|
return;
|
|
332
341
|
}
|
|
333
342
|
chunks.push(chunk);
|
|
@@ -522,7 +531,7 @@ function createPhoneHandler(deps) {
|
|
|
522
531
|
|
|
523
532
|
async function handle(req, res) {
|
|
524
533
|
const check = checkRequest(req, { token: deps.token, hosts: deps.hosts });
|
|
525
|
-
if (!check.ok) return send(res, check.status, check.status === 403 ? 'forbidden' : 'method not allowed', 'text/plain; charset=utf-8');
|
|
534
|
+
if (!check.ok) return send(res, check.status, check.status === 400 ? 'bad request' : check.status === 403 ? 'forbidden' : 'method not allowed', 'text/plain; charset=utf-8');
|
|
526
535
|
try {
|
|
527
536
|
if (req.method === 'GET' && check.path === '/') {
|
|
528
537
|
const nonce = crypto.randomBytes(16).toString('base64');
|
|
@@ -533,12 +542,13 @@ function createPhoneHandler(deps) {
|
|
|
533
542
|
if (req.method === 'GET' && check.path === '/state') return send(res, 200, state());
|
|
534
543
|
if (req.method === 'GET' && check.path === '/bet') return bet ? send(res, 200, bet.warm()) : send(res, 404, { ok: false, reason: 'not_found' });
|
|
535
544
|
if (req.method === 'POST' && (check.path === '/action' || check.path === '/undo' || (bet && check.path.startsWith('/bet/')))) {
|
|
536
|
-
if (
|
|
545
|
+
if (String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json') return send(res, 415, { ok: false, reason: 'json_only' });
|
|
537
546
|
let body;
|
|
538
547
|
try {
|
|
539
548
|
body = JSON.parse(await readBody(req) || '{}');
|
|
540
|
-
|
|
541
|
-
|
|
549
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) throw new Error('bad_body');
|
|
550
|
+
} catch (err) {
|
|
551
|
+
return send(res, err.message === 'too_large' ? 413 : 400, { ok: false, reason: 'bad_body' });
|
|
542
552
|
}
|
|
543
553
|
if (check.path.startsWith('/bet/')) {
|
|
544
554
|
const out = await betAction(check.path, body || {});
|
package/lib/site.js
CHANGED
|
@@ -111,8 +111,16 @@ function serveSite(dir, port = 4321) {
|
|
|
111
111
|
const port = server.address() && server.address().port;
|
|
112
112
|
const host = String(req.headers.host || '').toLowerCase();
|
|
113
113
|
if (![`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`].includes(host)) { res.writeHead(403); res.end('forbidden'); return; }
|
|
114
|
+
if (!['GET', 'HEAD'].includes(req.method)) { res.writeHead(405); res.end('method not allowed'); return; }
|
|
115
|
+
if (req.headers['transfer-encoding'] || Number(req.headers['content-length'] || 0) > 0) { res.writeHead(400); res.end('bad request'); return; }
|
|
114
116
|
let rel;
|
|
115
|
-
try {
|
|
117
|
+
try {
|
|
118
|
+
const target = req.url || '/';
|
|
119
|
+
new URL(target, 'http://localhost');
|
|
120
|
+
if (!target.startsWith('/') || target.startsWith('//') || /[\\#\0]/.test(target)) throw new Error('url');
|
|
121
|
+
decodeURIComponent(target);
|
|
122
|
+
rel = decodeURIComponent(target.split('?')[0]);
|
|
123
|
+
}
|
|
116
124
|
catch { res.writeHead(400); res.end('bad request'); return; }
|
|
117
125
|
// A null byte makes the fs calls below throw synchronously and crash the server.
|
|
118
126
|
if (rel.includes('\0')) { res.writeHead(400); res.end('bad request'); return; }
|
package/lib/zen-apply.js
CHANGED
|
@@ -536,8 +536,12 @@ function applyZen({ cwd = process.cwd(), fetch = true, freshMinutes, abandonedDa
|
|
|
536
536
|
const base = gitOut(['merge-base', 'HEAD', main], root);
|
|
537
537
|
const moved = moveKeepingNotes(root, ['checkout', '-q', main], main, tag, base || 'HEAD');
|
|
538
538
|
notes.push(...(moved.notes || []));
|
|
539
|
+
// A branch with nothing beyond origin is cleared as finished below, so
|
|
540
|
+
// only a branch with its own commits is called kept.
|
|
541
|
+
const own = branch && before.upstream ? gitOut(['rev-list', '--count', `${before.upstream}..${branch}`], root) : null;
|
|
539
542
|
if (moved.ok) {
|
|
540
|
-
|
|
543
|
+
const why = !branch ? '' : own === '0' ? ` (${branch} had nothing new)` : ` (${branch} is kept)`;
|
|
544
|
+
done.push(`moved back to ${main}${why}`);
|
|
541
545
|
branch = main;
|
|
542
546
|
} else {
|
|
543
547
|
left.push(`could not move to ${main}: ${moved.error}`);
|
package/package.json
CHANGED