atris 3.80.0 → 3.82.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.
- package/LICENSE +21 -0
- package/README.md +10 -0
- 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/auth.js +2 -2
- 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 +166 -2413
- 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 +109 -13
- package/commands/release.js +41 -10
- package/commands/security-review.js +5 -1
- package/commands/site-deploy.js +5 -1
- package/commands/status.js +1 -1
- package/commands/sync.js +12 -8
- package/commands/task.js +144 -118
- package/commands/unknowns.js +9 -5
- package/commands/visualize.js +1 -12
- package/commands/workflow.js +4 -3
- package/commands/worktree.js +68 -1602
- 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/computer-core.js +2447 -0
- package/lib/engine-ask.js +1 -1
- package/lib/engine-receipt-sweep.js +64 -0
- package/lib/file-lock.js +175 -0
- package/lib/fleet.js +1 -1
- package/lib/integrations-core.js +1695 -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-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/recall-exam.js +171 -0
- package/lib/roster-markdown.js +2 -2
- package/lib/runner-command.js +5 -5
- package/lib/security-audit-classes.js +20 -2
- package/lib/security-audit.js +31 -1
- package/lib/task-db.js +105 -37
- 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 +1517 -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 +10 -3
- package/utils/auth.js +18 -4
package/lib/engine-ask.js
CHANGED
|
@@ -246,7 +246,7 @@ function assertAskModelSupported(engine, model) {
|
|
|
246
246
|
function resolveAskModel(engine, modelName = '') {
|
|
247
247
|
const requested = String(modelName || '').trim();
|
|
248
248
|
if (requested) {
|
|
249
|
-
// A display name
|
|
249
|
+
// A spaced display name becomes an id only for the claude cli; every
|
|
250
250
|
// other engine gets its model name as typed.
|
|
251
251
|
return engine === 'claude' || engine === 'fable' || engine === 'haiku'
|
|
252
252
|
? claudeModelId(requested)
|
|
@@ -93,6 +93,70 @@ function sweepEngineAskReceipts(root, deps = {}) {
|
|
|
93
93
|
return summary;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Fleet and dispatch flights write a running receipt with the driver's pid
|
|
97
|
+
// and rewrite it when they finish. A crashed flight never rewrites it, so the
|
|
98
|
+
// receipt read as running forever. Flights run long, so the age bar is an hour
|
|
99
|
+
// and a live pid always keeps the receipt as it is.
|
|
100
|
+
const DISPATCH_RECEIPT_SCHEMA = 'atris.dispatch_receipt.v1';
|
|
101
|
+
const FLEET_GRACE_MS = 60 * 60 * 1000;
|
|
102
|
+
const FLEET_SWEEP_NOTE = 'stale receipt sweep marked this flight presumed dead: its process is gone and it never finished.';
|
|
103
|
+
|
|
104
|
+
function isFleetReceipt(name, receipt) {
|
|
105
|
+
if (receipt.schema === DISPATCH_RECEIPT_SCHEMA) return name.startsWith('dispatch-');
|
|
106
|
+
return !receipt.schema && name.startsWith('fleet-') && Array.isArray(receipt.results);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sweepFleetReceipts(root, deps = {}) {
|
|
110
|
+
const fsModule = deps.fs || fs;
|
|
111
|
+
const kill = deps.kill || process.kill;
|
|
112
|
+
const now = deps.now ? deps.now() : new Date();
|
|
113
|
+
const nowMs = now.getTime();
|
|
114
|
+
const graceMs = deps.graceMs === undefined ? FLEET_GRACE_MS : Number(deps.graceMs);
|
|
115
|
+
const summary = { scanned: 0, finalized: 0, skipped_alive: 0, skipped_young: 0 };
|
|
116
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
117
|
+
let names;
|
|
118
|
+
try {
|
|
119
|
+
names = fsModule.readdirSync(runsDir)
|
|
120
|
+
.filter((name) => name.endsWith('.json') && (name.startsWith('fleet-') || name.startsWith('dispatch-')))
|
|
121
|
+
.sort();
|
|
122
|
+
} catch {
|
|
123
|
+
return summary;
|
|
124
|
+
}
|
|
125
|
+
for (const name of names) {
|
|
126
|
+
const receiptPath = path.join(runsDir, name);
|
|
127
|
+
let receipt;
|
|
128
|
+
let stat;
|
|
129
|
+
try {
|
|
130
|
+
receipt = JSON.parse(fsModule.readFileSync(receiptPath, 'utf8'));
|
|
131
|
+
stat = fsModule.statSync(receiptPath);
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!receipt || typeof receipt !== 'object' || receipt.status !== 'running' || !isFleetReceipt(name, receipt)) continue;
|
|
136
|
+
summary.scanned += 1;
|
|
137
|
+
// Age from the last write, not the start: a long flight rewrites its
|
|
138
|
+
// receipt as tasks land.
|
|
139
|
+
const lastWriteMs = Math.max(receiptStartedMs(receipt, stat), stat.mtimeMs);
|
|
140
|
+
if (nowMs - lastWriteMs < graceMs) {
|
|
141
|
+
summary.skipped_young += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (processIsAlive(Number(receipt.pid), kill)) {
|
|
145
|
+
summary.skipped_alive += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
atomicWriteReceipt(receiptPath, {
|
|
149
|
+
...receipt,
|
|
150
|
+
status: 'presumed_dead',
|
|
151
|
+
swept_at: now.toISOString(),
|
|
152
|
+
note: FLEET_SWEEP_NOTE,
|
|
153
|
+
}, fsModule);
|
|
154
|
+
summary.finalized += 1;
|
|
155
|
+
}
|
|
156
|
+
return summary;
|
|
157
|
+
}
|
|
158
|
+
|
|
96
159
|
module.exports = {
|
|
97
160
|
sweepEngineAskReceipts,
|
|
161
|
+
sweepFleetReceipts,
|
|
98
162
|
};
|
package/lib/file-lock.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// One exclusive lock file per resource, shared by the loops that must not run
|
|
4
|
+
// twice at once (autoland tick, autopilot, mission, campaign run).
|
|
5
|
+
//
|
|
6
|
+
// - Create is exclusive (`wx`), so two takers never both win.
|
|
7
|
+
// - The file records { pid, host, token, at }. Release removes it only while
|
|
8
|
+
// the token (or the open file) is still ours, so a tick that lost its lock
|
|
9
|
+
// to a stale-steal never deletes the next owner's lock.
|
|
10
|
+
// - A stale lock is stolen by renaming it to a unique tombstone first. If the
|
|
11
|
+
// renamed file turns out not to be the one judged stale (someone replaced
|
|
12
|
+
// it in between), it is linked back without clobbering. Two waiters that
|
|
13
|
+
// both judged the same lock stale can no longer delete each other's fresh
|
|
14
|
+
// lock.
|
|
15
|
+
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
function pidAlive(pid) {
|
|
22
|
+
const n = Number(pid);
|
|
23
|
+
if (!Number.isInteger(n) || n <= 0) return false;
|
|
24
|
+
try {
|
|
25
|
+
process.kill(n, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return Boolean(error) && error.code === 'EPERM';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readRaw(file) {
|
|
33
|
+
try { return fs.readFileSync(file, 'utf8'); } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseHolder(raw) {
|
|
37
|
+
if (raw == null) return null;
|
|
38
|
+
try {
|
|
39
|
+
const value = JSON.parse(raw);
|
|
40
|
+
return value && typeof value === 'object' ? value : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sameFile(a, b) {
|
|
47
|
+
return Boolean(a && b) && a.ino === b.ino && a.dev === b.dev;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Remove `file` only if it is still the file described by `judged` (stat) and
|
|
51
|
+
// `judgedRaw` (contents). Returns true when the caller should retry create.
|
|
52
|
+
function stealStaleLock(file, judged, judgedRaw) {
|
|
53
|
+
const tomb = `${file}.stale-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
54
|
+
try {
|
|
55
|
+
fs.renameSync(file, tomb);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error && error.code === 'ENOENT') return true;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
let moved = null;
|
|
61
|
+
try { moved = fs.statSync(tomb); } catch {}
|
|
62
|
+
const movedRaw = readRaw(tomb);
|
|
63
|
+
if (!sameFile(moved, judged) || (judgedRaw != null && movedRaw !== judgedRaw)) {
|
|
64
|
+
// We grabbed a lock that was replaced after we judged it. Put it back
|
|
65
|
+
// without overwriting anything created since.
|
|
66
|
+
try { fs.linkSync(tomb, file); } catch {}
|
|
67
|
+
}
|
|
68
|
+
try { fs.unlinkSync(tomb); } catch {}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Decide whether a lock someone else holds may be taken over.
|
|
73
|
+
function judgeHolder(holder, ageMs, { host, isAlive, staleAfterMs, unreadableGraceMs }) {
|
|
74
|
+
if (!holder) return ageMs > unreadableGraceMs ? 'stale' : 'held';
|
|
75
|
+
const holderPid = Number(holder.pid);
|
|
76
|
+
const atMs = typeof holder.at === 'number' ? holder.at : Date.parse(holder.at || holder.started_at || '');
|
|
77
|
+
const heldFor = Number.isFinite(atMs) ? Date.now() - atMs : ageMs;
|
|
78
|
+
if (Number.isFinite(staleAfterMs) && heldFor > staleAfterMs) return 'stale';
|
|
79
|
+
if (holder.host && holder.host !== host) return 'held';
|
|
80
|
+
if (!Number.isInteger(holderPid) || holderPid <= 0) return ageMs > unreadableGraceMs ? 'stale' : 'held';
|
|
81
|
+
return isAlive(holderPid) ? 'held' : 'stale';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Returns { ok: true, file, token, fd?, release() } or { ok: false, file, holder }.
|
|
85
|
+
// Options:
|
|
86
|
+
// meta extra fields written into the lock record
|
|
87
|
+
// pid, host identity written into the record (defaults: this process)
|
|
88
|
+
// staleAfterMs a lock older than this is stale even with a live pid
|
|
89
|
+
// unreadableGraceMs an empty/unreadable lock younger than this is held
|
|
90
|
+
// (its owner may still be writing the record)
|
|
91
|
+
// keepOpen return the open fd (release then checks the inode)
|
|
92
|
+
// isAlive pid liveness probe (tests)
|
|
93
|
+
function acquireFileLock(file, options = {}) {
|
|
94
|
+
const {
|
|
95
|
+
meta = {},
|
|
96
|
+
pid = process.pid,
|
|
97
|
+
host = os.hostname(),
|
|
98
|
+
staleAfterMs = Infinity,
|
|
99
|
+
unreadableGraceMs = 5000,
|
|
100
|
+
keepOpen = false,
|
|
101
|
+
isAlive = pidAlive,
|
|
102
|
+
attempts = 5,
|
|
103
|
+
} = options;
|
|
104
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
105
|
+
const token = crypto.randomBytes(8).toString('hex');
|
|
106
|
+
const record = JSON.stringify({ ...meta, pid, host, token, at: new Date().toISOString() });
|
|
107
|
+
let lastHolder = null;
|
|
108
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
109
|
+
let fd;
|
|
110
|
+
try {
|
|
111
|
+
fd = fs.openSync(file, 'wx');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
114
|
+
}
|
|
115
|
+
if (fd !== undefined) {
|
|
116
|
+
try {
|
|
117
|
+
fs.writeSync(fd, record);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
try { fs.closeSync(fd); } catch {}
|
|
120
|
+
try { fs.unlinkSync(file); } catch {}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
let ino = null;
|
|
124
|
+
try { ino = fs.fstatSync(fd); } catch {}
|
|
125
|
+
if (!keepOpen) {
|
|
126
|
+
try { fs.closeSync(fd); } catch {}
|
|
127
|
+
fd = undefined;
|
|
128
|
+
}
|
|
129
|
+
const lock = { ok: true, file, token, fd, stat: ino, record };
|
|
130
|
+
lock.release = () => releaseFileLock(lock);
|
|
131
|
+
return lock;
|
|
132
|
+
}
|
|
133
|
+
let judged;
|
|
134
|
+
try {
|
|
135
|
+
judged = fs.statSync(file);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error && error.code === 'ENOENT') continue;
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
const raw = readRaw(file);
|
|
141
|
+
const holder = parseHolder(raw);
|
|
142
|
+
lastHolder = holder;
|
|
143
|
+
const verdict = judgeHolder(holder, Date.now() - judged.mtimeMs, { host, isAlive, staleAfterMs, unreadableGraceMs });
|
|
144
|
+
if (verdict === 'held') return { ok: false, file, holder };
|
|
145
|
+
stealStaleLock(file, judged, raw);
|
|
146
|
+
}
|
|
147
|
+
return { ok: false, file, holder: lastHolder || parseHolder(readRaw(file)) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Remove the lock only while it is still ours.
|
|
151
|
+
function releaseFileLock(lock) {
|
|
152
|
+
if (!lock || !lock.ok || lock.released) return false;
|
|
153
|
+
lock.released = true;
|
|
154
|
+
let ours = false;
|
|
155
|
+
if (lock.fd != null) {
|
|
156
|
+
try {
|
|
157
|
+
ours = sameFile(fs.fstatSync(lock.fd), fs.statSync(lock.file));
|
|
158
|
+
} catch {
|
|
159
|
+
ours = false;
|
|
160
|
+
}
|
|
161
|
+
try { fs.closeSync(lock.fd); } catch {}
|
|
162
|
+
} else {
|
|
163
|
+
const holder = parseHolder(readRaw(lock.file));
|
|
164
|
+
ours = Boolean(holder) && holder.token === lock.token;
|
|
165
|
+
}
|
|
166
|
+
if (!ours) return false;
|
|
167
|
+
try { fs.unlinkSync(lock.file); } catch { return false; }
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
acquireFileLock,
|
|
173
|
+
stealStaleLock,
|
|
174
|
+
pidAlive,
|
|
175
|
+
};
|
package/lib/fleet.js
CHANGED
|
@@ -179,7 +179,7 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
|
|
|
179
179
|
lines.push(
|
|
180
180
|
'Rules:',
|
|
181
181
|
mapNotesPath(worktreePath || process.cwd())
|
|
182
|
-
? '- Read atris/MAP.md first to locate the code, then atris/refs/MAP-NOTES.md for exact file:line refs; never guess file locations.'
|
|
182
|
+
? '- Read atris/MAP.md first to locate the code, then atris/refs/MAP-NOTES.md (or the area note it points to) for exact file:line refs; never guess file locations.'
|
|
183
183
|
: '- Read atris/MAP.md first to locate the code; never guess file locations.',
|
|
184
184
|
...SHARED_BRIEF_RULES,
|
|
185
185
|
'- Run git status first. Stage ONLY files you changed. Never revert or touch files another agent modified.',
|