atris 3.80.1 → 3.82.2
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/LICENSE +21 -0
- package/README.md +11 -1
- package/atris/atris.md +14 -0
- package/atris/features/README.md +15 -0
- package/atris/policies/README.md +24 -0
- package/atris/skills/README.md +61 -45
- package/atris/skills/design/SKILL.md +6 -5
- package/atris/skills/taste/SKILL.md +39 -0
- package/atris/skills/taste/notes.md +29 -0
- package/atris/skills/taste/principles.md +23 -0
- package/atris/skills/wiki/SKILL.md +14 -2
- package/atris/skills/wiki/index-lines.md +30 -0
- package/atris/skills/wiki/measure.md +20 -0
- package/atris/skills/wiki/note-shape.md +16 -0
- package/atris/skills/wiki/upkeep.md +16 -0
- package/atris/team/README.md +38 -0
- package/atris.md +14 -0
- package/bin/atris.js +1 -1
- package/commands/activate.js +1 -1
- package/commands/autoland.js +25 -26
- package/commands/autopilot-front.js +46 -1
- package/commands/autopilot.js +55 -25
- package/commands/clean.js +22 -11
- package/commands/computer.js +1 -1
- package/commands/doc-health.js +48 -1
- package/commands/improve.js +16 -3
- package/commands/init.js +4 -0
- package/commands/integrations.js +69 -1616
- package/commands/mission.js +112 -14
- package/commands/release.js +41 -10
- package/commands/status.js +1 -1
- package/commands/sync.js +12 -8
- package/commands/task.js +150 -137
- package/commands/unknowns.js +9 -5
- package/commands/visualize.js +1 -12
- package/commands/workflow.js +4 -3
- package/commands/worktree.js +107 -1610
- package/commands/xp.js +23 -2113
- package/commands/youtube.js +192 -3110
- package/lib/business-core.js +3 -4
- package/lib/campaign-run-lock.js +12 -1
- package/lib/engine-ask.js +1 -1
- package/lib/engine-receipt-sweep.js +64 -0
- package/lib/file-lock.js +175 -0
- package/lib/first-minute.js +37 -4
- package/lib/fleet.js +1 -1
- package/lib/integrations-core.js +1695 -0
- package/lib/intents.js +7 -0
- package/lib/json-file.js +22 -1
- package/lib/loop-doctor.js +36 -2
- package/lib/map-refs.js +68 -8
- package/lib/member-context.js +20 -1
- package/lib/member-engine.js +1 -1
- package/lib/member-map-fold.js +49 -11
- package/lib/member-map-inbox.js +2 -1
- package/lib/mission-core.js +9 -2
- package/lib/next-work-context.js +190 -0
- package/lib/recall-exam.js +171 -0
- package/lib/roster-markdown.js +2 -2
- package/lib/runner-command.js +5 -5
- package/lib/task-db.js +107 -38
- package/lib/taste-notes.js +35 -0
- package/lib/team-lineup.js +3 -3
- package/lib/todo.js +2 -0
- package/lib/wiki.js +11 -3
- package/lib/worktree-core.js +2032 -0
- package/lib/xp-core.js +2172 -0
- package/lib/youtube-core.js +3178 -0
- package/lib/zen-apply.js +3 -1
- package/package.json +2 -2
package/commands/autoland.js
CHANGED
|
@@ -9,7 +9,8 @@ const { localDay } = require('../lib/local-day');
|
|
|
9
9
|
const autoland = require('../lib/autoland');
|
|
10
10
|
const { gateForHuman, plainLandingReason } = require('../lib/voice-gate');
|
|
11
11
|
const { evaluateAutoAccept } = require('../lib/auto-accept-certified');
|
|
12
|
-
const { sweepEngineAskReceipts } = require('../lib/engine-receipt-sweep');
|
|
12
|
+
const { sweepEngineAskReceipts, sweepFleetReceipts } = require('../lib/engine-receipt-sweep');
|
|
13
|
+
const { acquireFileLock } = require('../lib/file-lock');
|
|
13
14
|
const { readSshAgents, findLeakedSshAgents, findAgentStartLines, agentStartLineText, stopLeakedSshAgents, closeLostRuns, readResourceHistory, errorReason, leakGrowthNote } = require('../lib/engine-resource');
|
|
14
15
|
const { validateRecentAskReceipts } = require('../lib/engine-validate');
|
|
15
16
|
const { operatorReady, hasAgentJargon, explainResult } = autoland;
|
|
@@ -731,15 +732,6 @@ function runDigest(root, args, { forceSend = false } = {}) {
|
|
|
731
732
|
return 0;
|
|
732
733
|
}
|
|
733
734
|
|
|
734
|
-
function pidAlive(pid) {
|
|
735
|
-
try {
|
|
736
|
-
process.kill(Number(pid), 0);
|
|
737
|
-
return true;
|
|
738
|
-
} catch {
|
|
739
|
-
return false;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
735
|
async function runTick(root, args) {
|
|
744
736
|
const json = args.includes('--json');
|
|
745
737
|
const policy = autoland.readPolicy(root);
|
|
@@ -756,24 +748,23 @@ async function runTick(root, args) {
|
|
|
756
748
|
// double-alarms, and double-digests. A stale lock (dead pid or >55min)
|
|
757
749
|
// never wedges the loop.
|
|
758
750
|
const lockPath = path.join(root, '.atris', 'state', 'autoland.tick.lock');
|
|
751
|
+
let lock;
|
|
759
752
|
try {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
771
|
-
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
772
|
-
} catch {}
|
|
753
|
+
lock = acquireFileLock(lockPath, { staleAfterMs: 55 * 60 * 1000 });
|
|
754
|
+
} catch {
|
|
755
|
+
lock = null;
|
|
756
|
+
}
|
|
757
|
+
if (lock && !lock.ok) {
|
|
758
|
+
receipt.skipped_reason = 'tick_already_running';
|
|
759
|
+
if (json) console.log(JSON.stringify(receipt));
|
|
760
|
+
else console.log('autoland tick: another tick is still running; skipped.');
|
|
761
|
+
return 0;
|
|
762
|
+
}
|
|
773
763
|
try {
|
|
774
764
|
return await runTickBody(root, { json, policy, receipt });
|
|
775
765
|
} finally {
|
|
776
|
-
|
|
766
|
+
// Only removes the lock while it is still this tick's.
|
|
767
|
+
if (lock) lock.release();
|
|
777
768
|
}
|
|
778
769
|
}
|
|
779
770
|
|
|
@@ -864,8 +855,10 @@ function digestTickStatus(receipt) {
|
|
|
864
855
|
|
|
865
856
|
function engineReceiptSweepNote(receipt) {
|
|
866
857
|
const finalized = Number(receipt.engine_receipt_sweep?.finalized) || 0;
|
|
867
|
-
|
|
868
|
-
|
|
858
|
+
const flights = Number(receipt.fleet_receipt_sweep?.finalized) || 0;
|
|
859
|
+
const engineNote = finalized ? `, finalized ${finalized} stale engine receipt${finalized === 1 ? '' : 's'} as presumed dead` : '';
|
|
860
|
+
const fleetNote = flights ? `, marked ${flights} crashed flight${flights === 1 ? '' : 's'} presumed dead` : '';
|
|
861
|
+
return engineNote + fleetNote;
|
|
869
862
|
}
|
|
870
863
|
|
|
871
864
|
// Read only: how many ssh-agents look leaked right now, and which readers
|
|
@@ -1041,6 +1034,12 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1041
1034
|
} catch (err) {
|
|
1042
1035
|
receipt.engine_receipt_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
1043
1036
|
}
|
|
1037
|
+
try {
|
|
1038
|
+
const fleetSweep = sweepFleetReceipts(root);
|
|
1039
|
+
if (fleetSweep.scanned > 0) receipt.fleet_receipt_sweep = fleetSweep;
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
receipt.fleet_receipt_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
1042
|
+
}
|
|
1044
1043
|
|
|
1045
1044
|
runSshAgentSweep(root, receipt);
|
|
1046
1045
|
runLostRunSweep(root, receipt);
|
|
@@ -12,6 +12,7 @@ const { wantsJson, hasYesFlag } = require('../lib/noninteractive');
|
|
|
12
12
|
const { isUnboundScratchFolder, refuseUnboundScratch } = require('../lib/scratch-root');
|
|
13
13
|
const { resolveWorkspaceRoot } = require('../lib/mission-root');
|
|
14
14
|
const { personName } = require('../lib/first-minute');
|
|
15
|
+
const { acquireFileLock } = require('../lib/file-lock');
|
|
15
16
|
|
|
16
17
|
const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
17
18
|
const DEFAULT_LEG_WALL_SECONDS = 3600;
|
|
@@ -21,6 +22,7 @@ const MAX_FAST_FAILS = 5;
|
|
|
21
22
|
|
|
22
23
|
function statePath(root) { return path.join(root, '.atris', 'state', 'autopilot.json'); }
|
|
23
24
|
function stopPath(root) { return path.join(root, '.atris', 'state', 'autopilot.stop'); }
|
|
25
|
+
function lockPath(root) { return path.join(root, '.atris', 'state', 'autopilot.lock'); }
|
|
24
26
|
|
|
25
27
|
function readState(root) {
|
|
26
28
|
try { return JSON.parse(fs.readFileSync(statePath(root), 'utf8')); } catch { return null; }
|
|
@@ -42,6 +44,26 @@ function requestStop(root) {
|
|
|
42
44
|
}
|
|
43
45
|
function clearStop(root) { try { fs.unlinkSync(stopPath(root)); } catch {} }
|
|
44
46
|
|
|
47
|
+
// When `autopilot stop` wrote its marker, in ms, or null without one.
|
|
48
|
+
function stopRequestedAtMs(root) {
|
|
49
|
+
let stat;
|
|
50
|
+
try { stat = fs.statSync(stopPath(root)); } catch { return null; }
|
|
51
|
+
let stamped = NaN;
|
|
52
|
+
try { stamped = Date.parse(fs.readFileSync(stopPath(root), 'utf8').trim()); } catch {}
|
|
53
|
+
return Number.isFinite(stamped) ? Math.max(stamped, stat.mtimeMs) : stat.mtimeMs;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A stop marker left over from before this start is cleared. One written
|
|
57
|
+
// after this process launched is a real stop that raced the start, so it is
|
|
58
|
+
// kept and honored. Returns true when such a stop is pending.
|
|
59
|
+
function clearStaleStop(root, startRequestedAtMs) {
|
|
60
|
+
const at = stopRequestedAtMs(root);
|
|
61
|
+
if (at == null) return false;
|
|
62
|
+
if (at >= startRequestedAtMs) return true;
|
|
63
|
+
clearStop(root);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
function pidAlive(pid) {
|
|
46
68
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
47
69
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
@@ -261,8 +283,28 @@ async function autopilotFront(args = []) {
|
|
|
261
283
|
console.log(`Autopilot already running (pid ${existing.pid}). Stop it first: atris autopilot stop`);
|
|
262
284
|
return 1;
|
|
263
285
|
}
|
|
286
|
+
// The state check above is read-then-write; two starts in the same instant
|
|
287
|
+
// both passed it. The lock file is created exclusively, so only one wins.
|
|
288
|
+
const lock = acquireFileLock(lockPath(root));
|
|
289
|
+
if (!lock.ok) {
|
|
290
|
+
console.log(`Autopilot already running (pid ${lock.holder?.pid || '?'}). Stop it first: atris autopilot stop`);
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
return await runFrontLoop(root, args, lock);
|
|
295
|
+
} finally {
|
|
296
|
+
lock.release();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
264
299
|
|
|
265
|
-
|
|
300
|
+
async function runFrontLoop(root, args, lock) {
|
|
301
|
+
// The moment this process launched. A stop issued after it is not stale.
|
|
302
|
+
const startRequestedAtMs = Date.now() - Math.round(process.uptime() * 1000);
|
|
303
|
+
if (clearStaleStop(root, startRequestedAtMs)) {
|
|
304
|
+
clearStop(root);
|
|
305
|
+
console.log('\nAutopilot off (stop requested). Legs run: 0.');
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
266
308
|
const budgetSeconds = runBudgetSeconds(args);
|
|
267
309
|
const legWallDefault = positiveNumber(readValueFlag(args, '--leg-wall')) || DEFAULT_LEG_WALL_SECONDS;
|
|
268
310
|
// Honor the legacy contract callers still speak: `--iterations=N` caps the
|
|
@@ -283,6 +325,8 @@ async function autopilotFront(args = []) {
|
|
|
283
325
|
console.log(`\nAutopilot off (${reason}). Legs run: ${state.legs}.`);
|
|
284
326
|
clearState(root);
|
|
285
327
|
clearStop(root);
|
|
328
|
+
// process.exit skips the caller's finally, so release here.
|
|
329
|
+
try { lock.release(); } catch {}
|
|
286
330
|
process.exit(130);
|
|
287
331
|
};
|
|
288
332
|
process.on('SIGINT', onSignal);
|
|
@@ -344,6 +388,7 @@ module.exports = {
|
|
|
344
388
|
stopRequested,
|
|
345
389
|
requestStop,
|
|
346
390
|
clearStop,
|
|
391
|
+
clearStaleStop,
|
|
347
392
|
readState,
|
|
348
393
|
writeState,
|
|
349
394
|
watchStopFile,
|
package/commands/autopilot.js
CHANGED
|
@@ -1867,24 +1867,41 @@ function isDoPhaseTimeoutMessage(message) {
|
|
|
1867
1867
|
* un-struck bullet whose normalized text contains the normalized title
|
|
1868
1868
|
* prefix; `- **T33:** …` becomes `- [x] **T33:** …`, `- [ ]` becomes `- [x]`.
|
|
1869
1869
|
* Returns true if a bullet was marked.
|
|
1870
|
+
*
|
|
1871
|
+
* A rendered TODO.md (it carries the "Regenerated from durable Atris task
|
|
1872
|
+
* state" marker) is a view of the task database, so there the job itself is
|
|
1873
|
+
* finished, the way `atris task done` finishes it, and the view re-rendered.
|
|
1874
|
+
* Returns false when no single open or claimed job matches: re-rendering alone
|
|
1875
|
+
* used to return true, and the tick reported a job advanced that never moved.
|
|
1870
1876
|
*/
|
|
1871
1877
|
function markTodoBulletDone(cwd, taskTitle) {
|
|
1878
|
+
const needle = taskMatchNeedle(taskTitle);
|
|
1879
|
+
let isRenderedView = false;
|
|
1872
1880
|
try {
|
|
1873
|
-
// A rendered TODO.md carries the "Regenerated from durable Atris task
|
|
1874
|
-
// state" marker; those are projections and never hand-edited. Legacy
|
|
1875
|
-
// hand-written TODO.md files (no marker) keep the bullet-mark fallback.
|
|
1876
1881
|
const renderedTodoPath = path.join(cwd, 'atris', 'TODO.md');
|
|
1877
|
-
|
|
1882
|
+
isRenderedView = fs.existsSync(renderedTodoPath)
|
|
1878
1883
|
&& fs.readFileSync(renderedTodoPath, 'utf8').includes('Regenerated from durable Atris task state');
|
|
1879
|
-
|
|
1884
|
+
} catch {
|
|
1885
|
+
isRenderedView = false;
|
|
1886
|
+
}
|
|
1887
|
+
if (isRenderedView) {
|
|
1888
|
+
if (!needle) return false;
|
|
1889
|
+
try {
|
|
1880
1890
|
require('node:sqlite');
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1891
|
+
const out = require('./task').completeTodoTaskByTitle(cwd, taskTitle, {
|
|
1892
|
+
matches: (title) => {
|
|
1893
|
+
const norm = normalizeForMatch(title);
|
|
1894
|
+
// The reverse test covers a tick title with a prefix ("T33: ..."); a short job title must not match by it.
|
|
1895
|
+
return Boolean(norm) && (norm.includes(needle) || (norm.length >= 12 && needle.includes(norm)));
|
|
1896
|
+
},
|
|
1897
|
+
actor: 'autopilot',
|
|
1898
|
+
proof: `autopilot tick finished "${String(taskTitle || '').slice(0, 200)}" and its completion receipt is in today's journal`,
|
|
1899
|
+
});
|
|
1900
|
+
return Boolean(out && out.completed);
|
|
1901
|
+
} catch {
|
|
1902
|
+
// Older Node runtimes without node:sqlite fall through to the markdown edit.
|
|
1883
1903
|
}
|
|
1884
|
-
} catch {
|
|
1885
|
-
// Older Node runtimes and legacy workspaces retain the markdown fallback.
|
|
1886
1904
|
}
|
|
1887
|
-
const needle = taskMatchNeedle(taskTitle);
|
|
1888
1905
|
if (!needle) return false;
|
|
1889
1906
|
for (const name of ['TODO.md', 'todo.md']) {
|
|
1890
1907
|
const todoPath = path.join(cwd, 'atris', name);
|
|
@@ -2211,26 +2228,38 @@ function logCompletion(description) {
|
|
|
2211
2228
|
ensureLogDirectory();
|
|
2212
2229
|
const { logFile, dateFormatted } = getLogPath();
|
|
2213
2230
|
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2231
|
+
// The next C number is read and the entry written as one step under a lock file, so two ticks
|
|
2232
|
+
// finishing at once cannot both take the same number. A lock that cannot be had in a few seconds
|
|
2233
|
+
// does not cost the entry: it is written anyway, as before.
|
|
2234
|
+
const write = () => {
|
|
2235
|
+
if (!fs.existsSync(logFile)) {
|
|
2236
|
+
createLogFile(logFile, dateFormatted);
|
|
2237
|
+
}
|
|
2217
2238
|
|
|
2218
|
-
|
|
2239
|
+
let content = fs.readFileSync(logFile, 'utf8');
|
|
2219
2240
|
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2241
|
+
const completionMatch = content.match(/\*\*C(\d+):/g);
|
|
2242
|
+
const nextId = completionMatch
|
|
2243
|
+
? Math.max(...completionMatch.map(m => parseInt(m.match(/\d+/)[0]))) + 1
|
|
2244
|
+
: 1;
|
|
2224
2245
|
|
|
2225
|
-
|
|
2246
|
+
const entry = `- **C${nextId}:** ${description} [reviewed]`;
|
|
2226
2247
|
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2248
|
+
// Readers look for "## Completed ✅" (and, since this fix, a bare "## Completed" too).
|
|
2249
|
+
if (/^## Completed\b/m.test(content)) {
|
|
2250
|
+
content = content.replace(/^(## Completed[^\n]*\n)/m, `$1\n${entry}\n`);
|
|
2251
|
+
} else {
|
|
2252
|
+
content += `\n## Completed ✅\n\n${entry}\n`;
|
|
2253
|
+
}
|
|
2232
2254
|
|
|
2233
|
-
|
|
2255
|
+
fs.writeFileSync(logFile, content);
|
|
2256
|
+
};
|
|
2257
|
+
try {
|
|
2258
|
+
require('../lib/handoff-wake').withFileLock(`${logFile}.lock`, write, { staleMs: 60 * 1000, waitMs: 5 * 1000 });
|
|
2259
|
+
} catch (err) {
|
|
2260
|
+
if (!/stayed locked/.test(String(err && err.message))) throw err;
|
|
2261
|
+
write();
|
|
2262
|
+
}
|
|
2234
2263
|
}
|
|
2235
2264
|
|
|
2236
2265
|
/**
|
|
@@ -4055,6 +4084,7 @@ module.exports = {
|
|
|
4055
4084
|
diffAdvancedRepoHeads,
|
|
4056
4085
|
reconcileTimedOutTick,
|
|
4057
4086
|
markTodoBulletDone,
|
|
4087
|
+
logCompletion,
|
|
4058
4088
|
findCompletionReceipt,
|
|
4059
4089
|
isDoPhaseTimeoutMessage,
|
|
4060
4090
|
validateVerifyCommandShape,
|
package/commands/clean.js
CHANGED
|
@@ -359,23 +359,34 @@ function findStaleTasks(atrisDir) {
|
|
|
359
359
|
* Detects both out-of-bounds AND drift (symbol moved to different line)
|
|
360
360
|
* Returns { healed: number, unhealable: array }
|
|
361
361
|
*/
|
|
362
|
-
// The boot map routes; atris/refs/MAP-NOTES.md holds the deep file:line refs
|
|
363
|
-
//
|
|
362
|
+
// The boot map routes; atris/refs/MAP-NOTES.md holds the deep file:line refs,
|
|
363
|
+
// or indexes area notes under atris/refs/map/ that hold them. Heal every one.
|
|
364
|
+
// Entries from a notes file carry `map` so reports can say where.
|
|
364
365
|
const MAP_NOTES_FILE = path.join('refs', 'MAP-NOTES.md');
|
|
366
|
+
const MAP_NOTES_DIR = path.join('refs', 'map');
|
|
367
|
+
|
|
368
|
+
function mapNoteFiles(atrisDir) {
|
|
369
|
+
const files = [path.join(atrisDir, MAP_NOTES_FILE)];
|
|
370
|
+
try {
|
|
371
|
+
for (const name of fs.readdirSync(path.join(atrisDir, MAP_NOTES_DIR)).sort()) {
|
|
372
|
+
if (name.endsWith('.md')) files.push(path.join(atrisDir, MAP_NOTES_DIR, name));
|
|
373
|
+
}
|
|
374
|
+
} catch { /* no area notes */ }
|
|
375
|
+
return files.filter((file) => { try { return fs.statSync(file).isFile(); } catch { return false; } });
|
|
376
|
+
}
|
|
365
377
|
|
|
366
378
|
function healBrokenMapRefs(cwd, atrisDir, dryRun = false, homeDir = os.homedir()) {
|
|
367
379
|
const mapFile = path.join(atrisDir, 'MAP.md');
|
|
368
380
|
if (!fs.existsSync(mapFile)) return { healed: 0, unhealable: [] };
|
|
369
381
|
const result = healMapFileRefs(cwd, mapFile, dryRun, homeDir);
|
|
370
|
-
const notesFile
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
};
|
|
382
|
+
for (const notesFile of mapNoteFiles(atrisDir)) {
|
|
383
|
+
const notes = healMapFileRefs(cwd, notesFile, dryRun, homeDir);
|
|
384
|
+
const map = path.relative(cwd, notesFile).split(path.sep).join('/');
|
|
385
|
+
result.healed += notes.healed;
|
|
386
|
+
result.unhealable = [...result.unhealable, ...notes.unhealable.map((entry) => ({ ...entry, map }))];
|
|
387
|
+
result.replacements = [...(result.replacements || []), ...notes.replacements.map((entry) => ({ ...entry, map }))];
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
379
390
|
}
|
|
380
391
|
|
|
381
392
|
function healMapFileRefs(cwd, mapFile, dryRun, homeDir) {
|
package/commands/computer.js
CHANGED
|
@@ -2105,7 +2105,7 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
|
|
|
2105
2105
|
console.log(' atris computer local');
|
|
2106
2106
|
console.log(' atris computer codex');
|
|
2107
2107
|
console.log(' atris computer --cloud');
|
|
2108
|
-
console.log(' atris computer --cloud --worker openai --model
|
|
2108
|
+
console.log(' atris computer --cloud --worker openai --model <model>');
|
|
2109
2109
|
console.log(' atris computer cloud');
|
|
2110
2110
|
console.log(' atris computer codeops');
|
|
2111
2111
|
console.log(' atris computer codeops status');
|
package/commands/doc-health.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { resolveWorkspaceRoot } = require('../lib/mission-root');
|
|
6
6
|
const { readText } = require('./brain');
|
|
7
7
|
const { checkMapDocs, fixMapDocs } = require('../lib/map-refs');
|
|
8
|
+
const { runRecallExam } = require('../lib/recall-exam');
|
|
8
9
|
|
|
9
10
|
const BOOT_FILES = [
|
|
10
11
|
'CLAUDE.md', 'AGENTS.md', 'atris/atris.md', 'atris/MAP.md',
|
|
@@ -357,12 +358,58 @@ function renderDocHealth(payload) {
|
|
|
357
358
|
return lines.join('\n');
|
|
358
359
|
}
|
|
359
360
|
|
|
361
|
+
function flagValue(args, name) {
|
|
362
|
+
const index = args.indexOf(name);
|
|
363
|
+
if (index >= 0 && args[index + 1] && !args[index + 1].startsWith('--')) return args[index + 1];
|
|
364
|
+
return args.find(arg => arg.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function renderRecall(report, summaryOnly) {
|
|
368
|
+
const pct = value => (value === null ? 'n/a' : `${Math.round(value * 100)}%`);
|
|
369
|
+
const lines = [
|
|
370
|
+
`recall exam: ${report.found}/${report.questions} found (${pct(report.found_share)})`,
|
|
371
|
+
`boot read first: ${report.boot_tokens} tokens`,
|
|
372
|
+
`files opened after boot: median ${report.files_opened.median}, mean ${report.files_opened.mean === null ? 'n/a' : report.files_opened.mean.toFixed(2)}`,
|
|
373
|
+
`wrong opens (opened but not on the path to the answer): total ${report.wrong_opens.total}, mean ${report.wrong_opens.mean === null ? 'n/a' : report.wrong_opens.mean.toFixed(2)}`,
|
|
374
|
+
`tokens opened after boot: median ${report.tokens_opened.median}, mean ${report.tokens_opened.mean}, median when found ${report.tokens_opened_when_found.median}`,
|
|
375
|
+
...table(['area', 'found', 'total'], Object.entries(report.by_area).sort().map(([area, row]) => [area, row.found, row.total])),
|
|
376
|
+
];
|
|
377
|
+
if (report.invalid_lines.length) lines.push(`invalid question lines skipped: ${report.invalid_lines.join(', ')}`);
|
|
378
|
+
if (!summaryOnly) {
|
|
379
|
+
lines.push('', ...table(['id', 'found', 'opened', 'wrong', 'tokens', 'answer in'],
|
|
380
|
+
report.results.map(row => [row.id || '-', row.found ? 'yes' : 'no', row.files_opened, row.wrong_opens, row.tokens_opened, row.found_in || '-'])));
|
|
381
|
+
}
|
|
382
|
+
return lines.join('\n');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function recallCommand(args, options, file) {
|
|
386
|
+
const root = resolveWorkspaceRoot(options.cwd || process.cwd());
|
|
387
|
+
const questionsFile = path.resolve(options.cwd || process.cwd(), file);
|
|
388
|
+
if (!stat(questionsFile)?.isFile()) {
|
|
389
|
+
console.log(`no recall questions at ${file}. one object per line: {"q":"...","needle":"exact answer phrase"}`);
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
const maxOpens = Number(flagValue(args, '--max-opens')) || undefined;
|
|
393
|
+
const report = runRecallExam(root, questionsFile, { bootFiles: BOOT_FILES, maxOpens });
|
|
394
|
+
// A held-out set is scored without echoing its questions back to the improver.
|
|
395
|
+
const summaryOnly = args.includes('--summary');
|
|
396
|
+
if (args.includes('--json')) {
|
|
397
|
+
const { results, ...totals } = report;
|
|
398
|
+
console.log(JSON.stringify({ ok: true, action: 'doc-health recall', ...totals, ...(summaryOnly ? {} : { results }) }, null, 2));
|
|
399
|
+
} else console.log(renderRecall(report, summaryOnly));
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
360
403
|
function docHealthCommand(args = [], options = {}) {
|
|
361
404
|
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
362
|
-
console.log('usage: atris doc-health [--json] [--questions <path>] [--fix-refs]');
|
|
405
|
+
console.log('usage: atris doc-health [--json] [--questions <path>] [--fix-refs] [--recall <path> [--summary] [--max-opens N]]');
|
|
363
406
|
console.log(' --fix-refs rewrite map line refs whose code moved; list the ones a human must fix');
|
|
407
|
+
console.log(' --recall follow links from the boot files to answer each question; report found, files opened, tokens read');
|
|
408
|
+
console.log(' --summary with --recall, print totals only so a held-out set stays unseen');
|
|
364
409
|
return 0;
|
|
365
410
|
}
|
|
411
|
+
const recallFile = flagValue(args, '--recall');
|
|
412
|
+
if (recallFile) return recallCommand(args, options, recallFile);
|
|
366
413
|
if (args.includes('--fix-refs')) {
|
|
367
414
|
const root = resolveWorkspaceRoot(options.cwd || process.cwd());
|
|
368
415
|
if (!stat(path.join(root, 'atris', 'MAP.md'))?.isFile()) {
|
package/commands/improve.js
CHANGED
|
@@ -96,6 +96,7 @@ function parseImproveArgs(argv = []) {
|
|
|
96
96
|
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
97
97
|
help: false,
|
|
98
98
|
history: false,
|
|
99
|
+
unknownCommand: null,
|
|
99
100
|
};
|
|
100
101
|
|
|
101
102
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -113,6 +114,7 @@ function parseImproveArgs(argv = []) {
|
|
|
113
114
|
if (a.startsWith('--timeout=')) { const v = Number(a.split('=')[1]); if (v > 0) opts.timeoutMs = Math.round(v * 1000); continue; }
|
|
114
115
|
// positional mode (plan|full|delegate)
|
|
115
116
|
if (!a.startsWith('-') && VALID_MODES.has(a)) { opts.mode = a; continue; }
|
|
117
|
+
if (i === 0 && !a.startsWith('-')) { opts.unknownCommand = a; continue; }
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
if (!VALID_MODES.has(opts.mode)) opts.mode = 'full';
|
|
@@ -300,7 +302,7 @@ function formatTickHistory(summary = {}) {
|
|
|
300
302
|
if (summary.rewardTrend && summary.rewardTrend.length) {
|
|
301
303
|
lines.push(` trend: ${summary.rewardTrend.map((r) => (r == null ? '·' : r)).join(' → ')}`);
|
|
302
304
|
}
|
|
303
|
-
if (!summary.ticks) lines.push(' (no ticks yet, run `atris improve`)');
|
|
305
|
+
if (!summary.ticks) lines.push(' (no ticks yet, run `atris improve tick`)');
|
|
304
306
|
return lines.join('\n');
|
|
305
307
|
}
|
|
306
308
|
|
|
@@ -1049,6 +1051,7 @@ const REVISION_WINDOW_HOURS = 72;
|
|
|
1049
1051
|
const REVISION_IGNORED_FILES = [
|
|
1050
1052
|
/^atris\/MAP\.md$/,
|
|
1051
1053
|
/^atris\/refs\/MAP-NOTES\.md$/,
|
|
1054
|
+
/^atris\/refs\/map\/[^/]+\.md$/,
|
|
1052
1055
|
/^test\/fast-tests\.txt$/,
|
|
1053
1056
|
/^atris\/logs\//,
|
|
1054
1057
|
/^atris\/runs\//,
|
|
@@ -1507,7 +1510,7 @@ Modes (positional or --mode):
|
|
|
1507
1510
|
|
|
1508
1511
|
Options:
|
|
1509
1512
|
--member <name> attribute the tick to a member (the loop's owner)
|
|
1510
|
-
--model <id> override the model (e.g.
|
|
1513
|
+
--model <id> override the model (e.g. sonnet)
|
|
1511
1514
|
--dry-run plan and run but do not commit
|
|
1512
1515
|
--no-fallback do not fall back to a local tick if the backend is down
|
|
1513
1516
|
--workspace <p> workspace path (default: cwd)
|
|
@@ -1523,7 +1526,11 @@ the backend is unreachable or you are not logged in.`);
|
|
|
1523
1526
|
}
|
|
1524
1527
|
|
|
1525
1528
|
function isBareVitalsArgs(argv = []) {
|
|
1526
|
-
|
|
1529
|
+
if (argv.length === 0 || (argv.length === 1 && argv[0] === '--json')) return true;
|
|
1530
|
+
if (argv[0] === 'status' || argv[0] === 'vitals') {
|
|
1531
|
+
return argv.length === 1 || (argv.length === 2 && argv[1] === '--json');
|
|
1532
|
+
}
|
|
1533
|
+
return false;
|
|
1527
1534
|
}
|
|
1528
1535
|
|
|
1529
1536
|
const LOOP_DOCTOR_OPEN_STATUSES = new Set(['planning', 'ready', 'running', 'paused', 'blocked']);
|
|
@@ -1693,6 +1700,12 @@ async function run(argv = [], deps = {}) {
|
|
|
1693
1700
|
const routedArgs = args[0] === 'tick' ? args.slice(1) : args;
|
|
1694
1701
|
const opts = parseImproveArgs(routedArgs);
|
|
1695
1702
|
if (opts.help) { showHelp(); return 0; }
|
|
1703
|
+
if (opts.unknownCommand) {
|
|
1704
|
+
const error = `unknown improve subcommand "${opts.unknownCommand}". try: atris improve, atris improve tick, atris improve history, atris improve doctor, or atris improve revisions.`;
|
|
1705
|
+
if (opts.json) console.log(JSON.stringify({ ok: false, error, subcommand: opts.unknownCommand }));
|
|
1706
|
+
else console.error(error);
|
|
1707
|
+
return 1;
|
|
1708
|
+
}
|
|
1696
1709
|
|
|
1697
1710
|
if (opts.history) {
|
|
1698
1711
|
const summary = summarizeTickHistory(readTickHistory(opts.workspace));
|
package/commands/init.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const { ensureProjectTasteFile } = require('../lib/taste-notes');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const { ensureMemberBundle } = require('../lib/member-scaffold');
|
|
4
5
|
const { ensureExperimentsFramework } = require('./experiments');
|
|
@@ -507,6 +508,9 @@ function initAtris() {
|
|
|
507
508
|
markReady('workspace', 'lessons.md', '✓ Created lessons.md');
|
|
508
509
|
}
|
|
509
510
|
|
|
511
|
+
// Project taste starts empty and is never overwritten; sync leaves it alone.
|
|
512
|
+
if (ensureProjectTasteFile(targetDir)) markReady('workspace', 'taste/TASTE.md', '✓ Created taste/TASTE.md');
|
|
513
|
+
|
|
510
514
|
// Create features directory and README
|
|
511
515
|
const featuresDir = path.join(targetDir, 'features');
|
|
512
516
|
const templatesDir = path.join(featuresDir, '_templates');
|