atris 3.52.0 → 3.55.0
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/FOR_AGENTS.md +7 -0
- package/README.md +1 -1
- package/atris/policies/ANTISLOP.md +2 -1
- package/atris/skills/copy-editor/SKILL.md +2 -1
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/engines/SKILL.md +8 -7
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +89 -40
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/ax +592 -91
- package/bin/atris +21 -0
- package/bin/atris.js +580 -378
- package/commands/api-key.js +170 -0
- package/commands/auth.js +453 -43
- package/commands/autoland.js +3 -1
- package/commands/autopilot-front.js +58 -11
- package/commands/avail.js +5 -4
- package/commands/balance.js +55 -0
- package/commands/bench.js +29 -1
- package/commands/brain.js +63 -12
- package/commands/brainstorm.js +18 -0
- package/commands/business.js +149 -1
- package/commands/chat-scan.js +17 -4
- package/commands/close.js +5 -1
- package/commands/computer.js +2 -2
- package/commands/doctor.js +174 -0
- package/commands/dream.js +7 -0
- package/commands/drill.js +34 -2
- package/commands/engine.js +47 -35
- package/commands/errors.js +58 -8
- package/commands/experiments.js +14 -0
- package/commands/feedback.js +61 -2
- package/commands/fleet-report.js +27 -7
- package/commands/fleet.js +98 -11
- package/commands/founder.js +60 -21
- package/commands/human-missions.js +79 -4
- package/commands/improve.js +54 -4
- package/commands/init.js +68 -92
- package/commands/install.js +40 -0
- package/commands/integrations.js +399 -30
- package/commands/interview.js +8 -0
- package/commands/land.js +48 -17
- package/commands/learn.js +29 -4
- package/commands/log.js +51 -0
- package/commands/member.js +68 -10
- package/commands/mission.js +339 -74
- package/commands/now.js +25 -6
- package/commands/orb.js +97 -2
- package/commands/pack.js +83 -9
- package/commands/playbook.js +381 -0
- package/commands/plugin.js +16 -0
- package/commands/radar.js +96 -40
- package/commands/recap.js +128 -21
- package/commands/release.js +102 -2
- package/commands/review.js +5 -10
- package/commands/revisions.js +55 -0
- package/commands/run-front.js +12 -4
- package/commands/scout.js +12 -1
- package/commands/search.js +123 -28
- package/commands/sign.js +2 -2
- package/commands/signup.js +12 -3
- package/commands/site-deploy.js +17 -4
- package/commands/site.js +2 -2
- package/commands/skill-eval.js +289 -0
- package/commands/skill.js +56 -6
- package/commands/social.js +603 -0
- package/commands/spaceship.js +69 -6
- package/commands/stream.js +8 -1
- package/commands/study.js +32 -19
- package/commands/sync.js +28 -22
- package/commands/task.js +418 -60
- package/commands/teach.js +72 -63
- package/commands/terminal.js +25 -9
- package/commands/topup.js +88 -0
- package/commands/truth.js +52 -6
- package/commands/usage.js +91 -0
- package/commands/verify.js +3 -2
- package/commands/version.js +3 -1
- package/commands/voice.js +5 -1
- package/commands/who.js +69 -11
- package/commands/wiki.js +7 -4
- package/commands/wish.js +76 -5
- package/commands/workflow.js +327 -145
- package/commands/worktree.js +1 -0
- package/commands/write.js +5 -0
- package/commands/x-search.js +340 -0
- package/commands/xp.js +5 -1
- package/commands/youtube.js +551 -33
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +10 -2
- package/lib/auto-lessons.js +186 -0
- package/lib/autoland.js +4 -2
- package/lib/chat-log-scan.js +7 -4
- package/lib/cli-json.js +77 -0
- package/lib/cli-scope.js +26 -0
- package/lib/codex-flight.js +1 -0
- package/lib/conductor-artifacts.js +1 -1
- package/lib/context-gatherer.js +11 -0
- package/lib/developer-api.js +116 -0
- package/lib/engine-ask.js +11 -3
- package/lib/engine-registry.js +32 -7
- package/lib/feedback-triage.js +274 -0
- package/lib/first-minute.js +398 -0
- package/lib/fleet.js +115 -34
- package/lib/functional-owner.js +22 -0
- package/lib/known-commands.js +31 -5
- package/lib/member-scaffold.js +197 -0
- package/lib/mission-ledger-compact.js +127 -0
- package/lib/mission-root.js +4 -2
- package/lib/mission-runtime-loop.js +30 -2
- package/lib/next-moves.js +34 -34
- package/lib/noninteractive.js +88 -0
- package/lib/permission-grants.js +10 -1
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/runner-command.js +5 -4
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +22 -2
- package/lib/task-proof.js +75 -9
- package/lib/task-receipt.js +5 -1
- package/lib/verifier-quality.js +69 -0
- package/lib/wish-audit.js +2 -2
- package/lib/wish-delegate.js +10 -1
- package/lib/workspace-scaffold.js +270 -0
- package/package.json +4 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/scripts/outbound-artifact-gate.js +227 -0
- package/utils/update-check.js +15 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { execFile } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const TRIAL_PROMPTS = [
|
|
9
|
+
'Write a brief status update telling your manager the deploy finished and one alert needs review.',
|
|
10
|
+
'Explain to a teammate in a few sentences why the cache change made startup faster.',
|
|
11
|
+
'Write a short note announcing that exports now run on a schedule.',
|
|
12
|
+
'Summarize for a non-engineer what changed in this week\'s release.',
|
|
13
|
+
'Write a two-paragraph answer on whether we should switch to the new logging library.',
|
|
14
|
+
'Draft a short reply declining a meeting and proposing async notes instead.',
|
|
15
|
+
];
|
|
16
|
+
const TRIAL_DRAFTS_PER_ARM = 2;
|
|
17
|
+
const TRIAL_BET = 0.5;
|
|
18
|
+
const JARGON_WORDS = /\b(?:leverage|seamless|robust|delve|synergy)\b/i;
|
|
19
|
+
|
|
20
|
+
function playbookFile(root = process.cwd()) {
|
|
21
|
+
return path.join(root, '.atris', 'state', 'playbook.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readPlaybook(root = process.cwd()) {
|
|
25
|
+
try {
|
|
26
|
+
const value = JSON.parse(fs.readFileSync(playbookFile(root), 'utf8'));
|
|
27
|
+
return { version: 1, rules: Array.isArray(value.rules) ? value.rules : [] };
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code === 'ENOENT') return { version: 1, rules: [] };
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writePlaybook(playbook, root = process.cwd()) {
|
|
35
|
+
const file = playbookFile(root);
|
|
36
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
37
|
+
fs.writeFileSync(file, `${JSON.stringify(playbook, null, 2)}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function optionValue(args, name) {
|
|
41
|
+
const index = args.indexOf(name);
|
|
42
|
+
if (index === -1) return null;
|
|
43
|
+
if (!args[index + 1] || args[index + 1].startsWith('--')) {
|
|
44
|
+
throw new Error(`${name} needs a value`);
|
|
45
|
+
}
|
|
46
|
+
return args[index + 1];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printHelp(output) {
|
|
50
|
+
output('usage: atris playbook [show] [--family <family>] [--json]');
|
|
51
|
+
output(' atris playbook add <family> <rule> [--source <source>]');
|
|
52
|
+
output(' atris playbook inject [--family <family>]');
|
|
53
|
+
output(' atris playbook remove <id>');
|
|
54
|
+
output(' atris playbook verify <id> --with <score> --without <score> [--method <method>]');
|
|
55
|
+
output(' atris playbook trial [--engine <cmd>] [--max-pairs <n>] [--alpha <a>]');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function filteredRules(playbook, family) {
|
|
59
|
+
return family ? playbook.rules.filter((entry) => entry.family === family) : playbook.rules;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mean(values) {
|
|
63
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sentenceCount(paragraph) {
|
|
67
|
+
const value = paragraph.trim();
|
|
68
|
+
if (!value) return 0;
|
|
69
|
+
const endings = value.match(/[.!?]+(?=\s|$)/g) || [];
|
|
70
|
+
return endings.length + (/[.!?]["')\]]*\s*$/.test(value) ? 0 : 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function gradeText(text) {
|
|
74
|
+
const value = String(text || '').trim();
|
|
75
|
+
const paragraphs = value ? value.split(/\n\s*\n/) : [''];
|
|
76
|
+
const checks = [
|
|
77
|
+
!value.includes('\u2014'),
|
|
78
|
+
paragraphs.every((paragraph) => sentenceCount(paragraph) <= 2),
|
|
79
|
+
!JARGON_WORDS.test(value),
|
|
80
|
+
!/\b[A-Z]{4,}\b/.test(value),
|
|
81
|
+
!value.includes('!'),
|
|
82
|
+
];
|
|
83
|
+
return checks.filter(Boolean).length / checks.length;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function bettingEvidence(differences, direction) {
|
|
87
|
+
const logEvidence = differences.reduce((sum, difference) => (
|
|
88
|
+
sum + Math.log(1 + (direction * TRIAL_BET * difference))
|
|
89
|
+
), 0);
|
|
90
|
+
return Math.exp(Math.min(logEvidence, Math.log(Number.MAX_VALUE)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function trialPrompt(rules, prompt) {
|
|
94
|
+
const system = [
|
|
95
|
+
'You are a sharp, plain-spoken assistant. Answer directly and briefly.',
|
|
96
|
+
rules.length ? `Follow every writing rule that applies:\n${rules.map((rule) => `- ${rule}`).join('\n')}` : '',
|
|
97
|
+
].filter(Boolean).join('\n\n');
|
|
98
|
+
return { system, argv: `${system}\n\nPrompt:\n${prompt}` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runShellEngine(command, prompt) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
execFile('/bin/sh', ['-c', `${command} "$1"`, 'atris-playbook-engine', prompt], {
|
|
104
|
+
timeout: 120000,
|
|
105
|
+
maxBuffer: 1024 * 1024,
|
|
106
|
+
}, (err, stdout) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
reject(new Error(`trial engine failed: ${err.message || err}`));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const reply = String(stdout || '').trim();
|
|
112
|
+
if (!reply) {
|
|
113
|
+
reject(new Error('trial engine returned an empty reply'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
resolve(reply);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runLocalEngine(system, prompt, fetchImpl) {
|
|
122
|
+
let lastError = null;
|
|
123
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetchImpl('http://localhost:11434/v1/chat/completions', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: { 'content-type': 'application/json' },
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
model: 'qwen2.5:7b',
|
|
130
|
+
temperature: 0.8,
|
|
131
|
+
messages: [
|
|
132
|
+
{ role: 'system', content: system },
|
|
133
|
+
{ role: 'user', content: prompt },
|
|
134
|
+
],
|
|
135
|
+
}),
|
|
136
|
+
signal: AbortSignal.timeout(120000),
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
lastError = new Error(`local trial engine returned status ${response.status}`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const body = await response.json();
|
|
143
|
+
const reply = String(body.choices?.[0]?.message?.content || '').trim();
|
|
144
|
+
if (reply) return reply;
|
|
145
|
+
lastError = new Error('local trial engine returned an empty reply');
|
|
146
|
+
} catch (err) {
|
|
147
|
+
lastError = err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`local trial engine failed: ${lastError?.message || 'no reply'}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function armScore(rules, prompt, options) {
|
|
154
|
+
const prepared = trialPrompt(rules, prompt);
|
|
155
|
+
const drafts = [];
|
|
156
|
+
for (let index = 0; index < TRIAL_DRAFTS_PER_ARM; index += 1) {
|
|
157
|
+
drafts.push(options.engine
|
|
158
|
+
? await runShellEngine(options.engine, prepared.argv)
|
|
159
|
+
: await runLocalEngine(prepared.system, prompt, options.fetchImpl));
|
|
160
|
+
}
|
|
161
|
+
return Math.max(...drafts.map(gradeText));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function numberedPrompt(index) {
|
|
165
|
+
const base = TRIAL_PROMPTS[index % TRIAL_PROMPTS.length];
|
|
166
|
+
const cycle = Math.floor(index / TRIAL_PROMPTS.length);
|
|
167
|
+
return cycle === 0 ? base : `${base} Variation ${cycle}.`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function trialStats(rule, alphaBudget) {
|
|
171
|
+
const differences = Array.isArray(rule.trial_diffs) ? rule.trial_diffs : [];
|
|
172
|
+
if (differences.length === 0) return null;
|
|
173
|
+
return {
|
|
174
|
+
pairs: differences.length,
|
|
175
|
+
difference: mean(differences),
|
|
176
|
+
killEvidence: bettingEvidence(differences, 1),
|
|
177
|
+
certifyEvidence: bettingEvidence(differences, -1),
|
|
178
|
+
threshold: 1 / alphaBudget,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function applyTrialVerdict(rule, stats, options) {
|
|
183
|
+
const quarantined = stats.killEvidence >= stats.threshold;
|
|
184
|
+
const certified = stats.certifyEvidence >= stats.threshold;
|
|
185
|
+
if (!quarantined && !certified) return null;
|
|
186
|
+
const withScores = Array.isArray(rule.trial_scores_with) ? rule.trial_scores_with : [];
|
|
187
|
+
const withoutScores = Array.isArray(rule.trial_scores_without) ? rule.trial_scores_without : [];
|
|
188
|
+
rule.quarantined = quarantined;
|
|
189
|
+
rule.verified = {
|
|
190
|
+
method: 'paired-trial',
|
|
191
|
+
score_with: withScores.length ? mean(withScores) : null,
|
|
192
|
+
score_without: withoutScores.length ? mean(withoutScores) : null,
|
|
193
|
+
pairs: stats.pairs,
|
|
194
|
+
mean_difference: stats.difference,
|
|
195
|
+
kill_evidence: stats.killEvidence,
|
|
196
|
+
certify_evidence: stats.certifyEvidence,
|
|
197
|
+
evidence_threshold: stats.threshold,
|
|
198
|
+
alpha: options.alphaBudget,
|
|
199
|
+
at: options.now(),
|
|
200
|
+
};
|
|
201
|
+
return quarantined ? 'quarantined' : 'certified';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function runRuleTrial(rule, playbook, options) {
|
|
205
|
+
const activeRules = playbook.rules.filter((entry) => entry !== rule && !entry.quarantined);
|
|
206
|
+
const rulesWithout = activeRules.map((entry) => entry.rule);
|
|
207
|
+
const rulesWith = [...rulesWithout, rule.rule];
|
|
208
|
+
rule.trial_diffs = Array.isArray(rule.trial_diffs) ? rule.trial_diffs : [];
|
|
209
|
+
rule.trial_scores_with = Array.isArray(rule.trial_scores_with) ? rule.trial_scores_with : [];
|
|
210
|
+
rule.trial_scores_without = Array.isArray(rule.trial_scores_without) ? rule.trial_scores_without : [];
|
|
211
|
+
options.output(`on trial: ${rule.id} (${rule.trial_diffs.length} prior pairs)`);
|
|
212
|
+
|
|
213
|
+
let stats = trialStats(rule, options.alphaBudget);
|
|
214
|
+
let verdict = stats ? applyTrialVerdict(rule, stats, options) : null;
|
|
215
|
+
while (!verdict && rule.trial_diffs.length < options.maxPairs) {
|
|
216
|
+
const start = rule.trial_diffs.length;
|
|
217
|
+
const prompt = numberedPrompt(start);
|
|
218
|
+
const [scoreWith, scoreWithout] = await Promise.all([
|
|
219
|
+
armScore(rulesWith, prompt, options),
|
|
220
|
+
armScore(rulesWithout, prompt, options),
|
|
221
|
+
]);
|
|
222
|
+
rule.trial_scores_with.push(scoreWith);
|
|
223
|
+
rule.trial_scores_without.push(scoreWithout);
|
|
224
|
+
rule.trial_diffs.push(Number((scoreWithout - scoreWith).toFixed(6)));
|
|
225
|
+
stats = trialStats(rule, options.alphaBudget);
|
|
226
|
+
verdict = applyTrialVerdict(rule, stats, options);
|
|
227
|
+
writePlaybook(playbook, options.root);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (verdict) {
|
|
231
|
+
writePlaybook(playbook, options.root);
|
|
232
|
+
options.output(`rule ${verdict}: ${rule.id} after ${stats.pairs} pairs`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
options.output(`rule undecided after ${rule.trial_diffs.length} pairs: ${rule.id}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function runTrials(playbook, rest, options) {
|
|
239
|
+
const rawMaxPairs = optionValue(rest, '--max-pairs');
|
|
240
|
+
const rawAlpha = optionValue(rest, '--alpha');
|
|
241
|
+
const maxPairs = rawMaxPairs === null ? 30 : Number(rawMaxPairs);
|
|
242
|
+
const alpha = rawAlpha === null ? 0.05 : Number(rawAlpha);
|
|
243
|
+
if (!Number.isInteger(maxPairs) || maxPairs < 1) throw new Error('--max-pairs needs a positive integer');
|
|
244
|
+
if (!Number.isFinite(alpha) || alpha <= 0 || alpha >= 1) throw new Error('--alpha needs a number between 0 and 1');
|
|
245
|
+
const engine = optionValue(rest, '--engine');
|
|
246
|
+
const unresolved = playbook.rules.filter((rule) => !rule.verified && rule.quarantined !== true);
|
|
247
|
+
if (unresolved.length === 0) {
|
|
248
|
+
options.output('no unresolved rules');
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
const alphaBudget = alpha / Math.max(1, playbook.rules.length);
|
|
252
|
+
for (const rule of unresolved) {
|
|
253
|
+
await runRuleTrial(rule, playbook, {
|
|
254
|
+
...options,
|
|
255
|
+
engine,
|
|
256
|
+
maxPairs,
|
|
257
|
+
alphaBudget,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function playbookCommand(args = [], options = {}) {
|
|
264
|
+
const root = options.root || process.cwd();
|
|
265
|
+
const output = options.output || console.log;
|
|
266
|
+
const now = options.now || (() => new Date().toISOString());
|
|
267
|
+
const makeId = options.makeId || (() => crypto.randomUUID().slice(0, 8));
|
|
268
|
+
const subcommand = args[0] && !args[0].startsWith('--') ? args[0] : 'show';
|
|
269
|
+
const rest = subcommand === 'show' && args[0] !== 'show' ? args : args.slice(1);
|
|
270
|
+
|
|
271
|
+
if (args.includes('--help') || args.includes('-h') || subcommand === 'help') {
|
|
272
|
+
printHelp(output);
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const playbook = readPlaybook(root);
|
|
277
|
+
if (subcommand === 'trial') {
|
|
278
|
+
return runTrials(playbook, rest, {
|
|
279
|
+
root,
|
|
280
|
+
output,
|
|
281
|
+
now,
|
|
282
|
+
fetchImpl: options.fetch || globalThis.fetch,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (subcommand === 'show') {
|
|
286
|
+
const family = optionValue(rest, '--family');
|
|
287
|
+
const rules = filteredRules(playbook, family);
|
|
288
|
+
if (rest.includes('--json')) {
|
|
289
|
+
output(JSON.stringify({ version: 1, rules }, null, 2));
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
if (rules.length === 0) {
|
|
293
|
+
output('no style rules saved');
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
const families = [...new Set(rules.map((entry) => entry.family))];
|
|
297
|
+
for (const name of families) {
|
|
298
|
+
output(`${name}:`);
|
|
299
|
+
for (const entry of rules.filter((rule) => rule.family === name)) {
|
|
300
|
+
const state = entry.quarantined ? ' (quarantined)' : '';
|
|
301
|
+
output(`- ${entry.id}: ${entry.rule}${state}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (subcommand === 'add') {
|
|
308
|
+
const family = rest[0];
|
|
309
|
+
const rule = rest[1];
|
|
310
|
+
if (!family || !rule || family.startsWith('--') || rule.startsWith('--')) {
|
|
311
|
+
throw new Error('add needs a family and rule');
|
|
312
|
+
}
|
|
313
|
+
const duplicate = playbook.rules.find((entry) => entry.family === family && entry.rule === rule);
|
|
314
|
+
if (duplicate) {
|
|
315
|
+
output(`rule already exists: ${duplicate.id}`);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
const entry = {
|
|
319
|
+
id: makeId(),
|
|
320
|
+
family,
|
|
321
|
+
rule,
|
|
322
|
+
source: optionValue(rest, '--source') || '',
|
|
323
|
+
verified: null,
|
|
324
|
+
created_at: now(),
|
|
325
|
+
};
|
|
326
|
+
playbook.rules.push(entry);
|
|
327
|
+
writePlaybook(playbook, root);
|
|
328
|
+
output(`rule added: ${entry.id}`);
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (subcommand === 'inject') {
|
|
333
|
+
const family = optionValue(rest, '--family');
|
|
334
|
+
const rules = filteredRules(playbook, family).filter((entry) => !entry.quarantined);
|
|
335
|
+
if (rules.length === 0) return 0;
|
|
336
|
+
output('Workspace style rules (follow every rule that applies):');
|
|
337
|
+
for (const entry of rules) output(`- ${entry.rule}`);
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (subcommand === 'remove') {
|
|
342
|
+
const id = rest[0];
|
|
343
|
+
if (!id) throw new Error('remove needs a rule id');
|
|
344
|
+
const index = playbook.rules.findIndex((entry) => entry.id === id);
|
|
345
|
+
if (index === -1) throw new Error(`rule not found: ${id}`);
|
|
346
|
+
playbook.rules.splice(index, 1);
|
|
347
|
+
writePlaybook(playbook, root);
|
|
348
|
+
output(`rule removed: ${id}`);
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (subcommand === 'verify') {
|
|
353
|
+
const id = rest[0];
|
|
354
|
+
const rawScoreWith = optionValue(rest, '--with');
|
|
355
|
+
const rawScoreWithout = optionValue(rest, '--without');
|
|
356
|
+
const scoreWith = Number(rawScoreWith);
|
|
357
|
+
const scoreWithout = Number(rawScoreWithout);
|
|
358
|
+
if (!id) throw new Error('verify needs a rule id');
|
|
359
|
+
if (rawScoreWith === null || rawScoreWithout === null ||
|
|
360
|
+
!Number.isFinite(scoreWith) || !Number.isFinite(scoreWithout)) {
|
|
361
|
+
throw new Error('verify needs numeric --with and --without scores');
|
|
362
|
+
}
|
|
363
|
+
const entry = playbook.rules.find((rule) => rule.id === id);
|
|
364
|
+
if (!entry) throw new Error(`rule not found: ${id}`);
|
|
365
|
+
entry.verified = {
|
|
366
|
+
method: optionValue(rest, '--method') || 'replay',
|
|
367
|
+
score_with: scoreWith,
|
|
368
|
+
score_without: scoreWithout,
|
|
369
|
+
at: now(),
|
|
370
|
+
};
|
|
371
|
+
if (scoreWith < scoreWithout) entry.quarantined = true;
|
|
372
|
+
else delete entry.quarantined;
|
|
373
|
+
writePlaybook(playbook, root);
|
|
374
|
+
output(entry.quarantined ? `rule quarantined: ${id}` : `rule verified: ${id}`);
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
throw new Error(`unknown playbook command: ${subcommand}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
module.exports = { playbookCommand, playbookFile, readPlaybook, gradeText };
|
package/commands/plugin.js
CHANGED
|
@@ -334,6 +334,10 @@ function showPluginInfo() {
|
|
|
334
334
|
// --- PUBLISH subcommand ---
|
|
335
335
|
|
|
336
336
|
function publishPlugin(...args) {
|
|
337
|
+
if (args.some((arg) => arg === '--help' || arg === '-h' || arg === 'help')) {
|
|
338
|
+
console.error('Usage: atris plugin publish [--repo=path]');
|
|
339
|
+
process.exit(2);
|
|
340
|
+
}
|
|
337
341
|
const projectDir = process.cwd();
|
|
338
342
|
const skillsDir = path.join(projectDir, 'atris', 'skills');
|
|
339
343
|
|
|
@@ -446,6 +450,18 @@ function publishPlugin(...args) {
|
|
|
446
450
|
// --- Main Dispatcher ---
|
|
447
451
|
|
|
448
452
|
function pluginCommand(subcommand, ...args) {
|
|
453
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help'
|
|
454
|
+
|| args.some((a) => a === '--help' || a === '-h' || a === 'help')) {
|
|
455
|
+
console.log('');
|
|
456
|
+
console.log('Usage: atris plugin <subcommand>');
|
|
457
|
+
console.log('');
|
|
458
|
+
console.log('Subcommands:');
|
|
459
|
+
console.log(' build [--output=path] - Package skills into .plugin for Cowork');
|
|
460
|
+
console.log(' publish [--repo=path] - Sync skills to marketplace repo and push');
|
|
461
|
+
console.log(' info - Preview what will be included');
|
|
462
|
+
console.log('');
|
|
463
|
+
process.exit(0);
|
|
464
|
+
}
|
|
449
465
|
switch (subcommand) {
|
|
450
466
|
case 'build':
|
|
451
467
|
buildPlugin(...args);
|
package/commands/radar.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execFileSync } = require('child_process');
|
|
7
|
+
const { parseScopeFlag, pathUnderRoot } = require('../lib/cli-scope');
|
|
7
8
|
|
|
8
9
|
function safeJson(text, fallback = null) {
|
|
9
10
|
try { return JSON.parse(text); } catch { return fallback; }
|
|
@@ -54,23 +55,39 @@ function agentTypeForCommand(command) {
|
|
|
54
55
|
if (/(^|\s|\/)claude(\s|$)/.test(cmd) && !/Claude\.app/.test(cmd)) return 'claude';
|
|
55
56
|
if (/(^|\s|\/)opencode(\s|$)/.test(cmd)) return 'opencode';
|
|
56
57
|
if (/(^|\s|\/)devin(\s|$)/.test(cmd)) return 'devin';
|
|
58
|
+
if (/(^|\s|\/)agy(\s|$)/.test(cmd)) return 'agy';
|
|
57
59
|
if (/(^|\s|\/)droid(\s|$)/.test(cmd)) return 'droid';
|
|
58
60
|
return null;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
// Resolve cwd for many pids at once: one lsof call on darwin instead of one
|
|
64
|
+
// per process. Returns a Map of pid (string) -> cwd; missing pids stay absent.
|
|
65
|
+
function processCwds(pids, deps) {
|
|
62
66
|
const { platform, execFile } = deps;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const byPid = new Map();
|
|
68
|
+
if (!pids.length) return byPid;
|
|
69
|
+
if (platform === 'linux') {
|
|
70
|
+
for (const pid of pids) {
|
|
71
|
+
try { byPid.set(String(pid), deps.readlink(`/proc/${pid}/cwd`)); } catch {}
|
|
66
72
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
return byPid;
|
|
74
|
+
}
|
|
75
|
+
if (platform === 'darwin') {
|
|
76
|
+
let out = '';
|
|
77
|
+
try {
|
|
78
|
+
out = execFile('lsof', ['-a', '-d', 'cwd', '-Fn', '-p', pids.map(String).join(',')], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// lsof exits non-zero when any pid in the batch is gone; keep the rows it
|
|
81
|
+
// did print instead of losing every cwd.
|
|
82
|
+
out = error && error.stdout ? error.stdout : '';
|
|
71
83
|
}
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
let currentPid = '';
|
|
85
|
+
for (const line of String(out).split(/\r?\n/)) {
|
|
86
|
+
if (line.startsWith('p')) currentPid = line.slice(1).trim();
|
|
87
|
+
else if (line.startsWith('n') && currentPid) byPid.set(currentPid, line.slice(1));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return byPid;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
function gitBranch(cwd, execFile) {
|
|
@@ -93,22 +110,28 @@ function collectAgents(deps) {
|
|
|
93
110
|
.map(row => ({ ...row, agent: agentTypeForCommand(row.command) }))
|
|
94
111
|
.filter(row => row.agent);
|
|
95
112
|
const parentPids = new Set(agentRows.map(row => String(row.ppid || '')).filter(Boolean));
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const topRows = agentRows.filter(row => !parentPids.has(String(row.pid)));
|
|
114
|
+
const cwdByPid = processCwds(topRows.map(row => row.pid), deps);
|
|
115
|
+
const branchByCwd = new Map();
|
|
116
|
+
const branchFor = (cwd) => {
|
|
117
|
+
if (!cwd) return '';
|
|
118
|
+
if (!branchByCwd.has(cwd)) branchByCwd.set(cwd, gitBranch(cwd, deps.execFile));
|
|
119
|
+
return branchByCwd.get(cwd);
|
|
120
|
+
};
|
|
121
|
+
return topRows.map(row => {
|
|
122
|
+
const cwd = cwdByPid.get(String(row.pid)) || '';
|
|
123
|
+
return {
|
|
124
|
+
pid: row.pid,
|
|
125
|
+
agent: row.agent,
|
|
126
|
+
command: row.command,
|
|
127
|
+
status: row.stat.includes('Z') ? 'zombie' : row.stat.includes('T') ? 'stopped' : 'active',
|
|
128
|
+
cwd,
|
|
129
|
+
repo: repoLabel(cwd),
|
|
130
|
+
branch: branchFor(cwd),
|
|
131
|
+
cpu: row.cpu,
|
|
132
|
+
mem: row.mem,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
112
135
|
}
|
|
113
136
|
|
|
114
137
|
function loadTasks(root, deps) {
|
|
@@ -287,8 +310,11 @@ function loadTeam(root, deps) {
|
|
|
287
310
|
};
|
|
288
311
|
}
|
|
289
312
|
|
|
290
|
-
function
|
|
291
|
-
|
|
313
|
+
function loadScorecards(root, deps) {
|
|
314
|
+
return readJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps.readFile, deps.exists);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function loadBrain(root, deps, scorecards = loadScorecards(root, deps)) {
|
|
292
318
|
const operatorDir = path.join(root, '.atris', 'state', 'operator-scorecards');
|
|
293
319
|
let operatorScorecards = 0;
|
|
294
320
|
try {
|
|
@@ -316,7 +342,7 @@ function countDirectoryEntries(dir, deps, predicate = () => true) {
|
|
|
316
342
|
return listNames(dir, deps).filter(predicate).length;
|
|
317
343
|
}
|
|
318
344
|
|
|
319
|
-
function loadBusinessCollaboration(root, deps, team = {}) {
|
|
345
|
+
function loadBusinessCollaboration(root, deps, team = {}, scorecardCount = countJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps)) {
|
|
320
346
|
const business = readJsonFile(path.join(root, '.atris', 'business.json'), deps, null);
|
|
321
347
|
const runtime = readJsonFile(path.join(root, '.atris', 'state', 'runtime.json'), deps, null);
|
|
322
348
|
const sync = readJsonFile(path.join(root, '.atris', 'state', '_sync.json'), deps, null);
|
|
@@ -335,7 +361,7 @@ function loadBusinessCollaboration(root, deps, team = {}) {
|
|
|
335
361
|
const localReceipts = countDirectoryEntries(path.join(root, '.atris', 'receipts'), deps, name => /\.(json|md|txt)$/i.test(name));
|
|
336
362
|
const events = countJsonLines(path.join(root, '.atris', 'state', 'events.jsonl'), deps);
|
|
337
363
|
const episodes = countJsonLines(path.join(root, '.atris', 'state', 'episodes.jsonl'), deps);
|
|
338
|
-
const scorecards =
|
|
364
|
+
const scorecards = scorecardCount;
|
|
339
365
|
const computerDirs = countDirectoryEntries(path.join(root, 'atris', 'computers'), deps, name => !name.startsWith('.'));
|
|
340
366
|
const hasOnboarding = ingestPacks > 0 || starterBriefs > 0 || onePagers > 0;
|
|
341
367
|
const hasProofLoop = events > 0 || episodes > 0 || scorecards > 0 || localReceipts > 0;
|
|
@@ -770,6 +796,7 @@ function nextAction(tasks, missions, worktrees, agents, os = {}) {
|
|
|
770
796
|
|
|
771
797
|
function collectRadar(options = {}) {
|
|
772
798
|
const root = options.root || process.cwd();
|
|
799
|
+
const scopeKind = options.scope === 'global' ? 'global' : 'workspace';
|
|
773
800
|
const deps = {
|
|
774
801
|
execFile: options.execFileSync || execFileSync,
|
|
775
802
|
readFile: options.readFileSync || fs.readFileSync,
|
|
@@ -785,13 +812,23 @@ function collectRadar(options = {}) {
|
|
|
785
812
|
const identityCache = new Map();
|
|
786
813
|
const missions = loadMissions(root, deps, nowMs);
|
|
787
814
|
identityCache.set(root, { missions, missionLocks: loadMissionLocks(path.join(root, '.atris', 'state'), deps), memberLocks: loadMemberLoopLocks(path.join(root, '.atris', 'state'), deps) });
|
|
788
|
-
|
|
789
|
-
|
|
815
|
+
let worktrees = loadWorktrees(root, deps);
|
|
816
|
+
if (scopeKind !== 'global') {
|
|
817
|
+
// Refuse other-repo dirty worktrees in the default workspace view.
|
|
818
|
+
worktrees = worktrees.filter((wt) => pathUnderRoot(wt.path, root));
|
|
819
|
+
}
|
|
820
|
+
const branchIdentityCache = new Map();
|
|
821
|
+
const branchIdentityFor = (cwd) => {
|
|
822
|
+
if (!cwd) return null;
|
|
823
|
+
if (!branchIdentityCache.has(cwd)) branchIdentityCache.set(cwd, branchIdentityAtCwd(cwd, deps.execFile));
|
|
824
|
+
return branchIdentityCache.get(cwd);
|
|
825
|
+
};
|
|
826
|
+
let agents = collectAgents(deps).map(agent => {
|
|
790
827
|
const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
|
|
791
828
|
const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
|
|
792
829
|
const identity = taskWorkspaceRoot ? loadWorkspaceIdentity(taskWorkspaceRoot, deps, identityCache, nowMs) : { missions: [], missionLocks: [], memberLocks: [] };
|
|
793
830
|
const sidecar = loadAgentWorktreeSidecar(agent.cwd, deps);
|
|
794
|
-
const branchIdentity =
|
|
831
|
+
const branchIdentity = branchIdentityFor(agent.cwd);
|
|
795
832
|
const sessionId = parseSessionIdFromCommand(agent.command);
|
|
796
833
|
const interactive = agent.agent === 'claude' && isInteractiveClaudeCommand(agent.command);
|
|
797
834
|
const resolved = resolveAgentTaskBinding({
|
|
@@ -823,21 +860,38 @@ function collectRadar(options = {}) {
|
|
|
823
860
|
task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks, resolved),
|
|
824
861
|
};
|
|
825
862
|
});
|
|
863
|
+
if (scopeKind !== 'global') {
|
|
864
|
+
agents = agents.filter((agent) => pathUnderRoot(agent.cwd, root)
|
|
865
|
+
|| (agent.task_workspace && pathUnderRoot(findTaskWorkspaceRoot(agent.cwd, deps), root)));
|
|
866
|
+
}
|
|
867
|
+
const scorecards = loadScorecards(root, deps);
|
|
826
868
|
const osState = {
|
|
827
869
|
xp: loadXp(root, deps),
|
|
828
870
|
team: loadTeam(root, deps),
|
|
829
|
-
brain: loadBrain(root, deps),
|
|
871
|
+
brain: loadBrain(root, deps, scorecards),
|
|
830
872
|
swarlo: loadSwarlo(tasks),
|
|
831
873
|
loop: loadLoop(missions, root, deps),
|
|
832
874
|
};
|
|
833
|
-
osState.business = loadBusinessCollaboration(root, deps, osState.team);
|
|
834
|
-
return {
|
|
875
|
+
osState.business = loadBusinessCollaboration(root, deps, osState.team, scorecards.length);
|
|
876
|
+
return {
|
|
877
|
+
scope: scopeKind,
|
|
878
|
+
root,
|
|
879
|
+
generated_at: new Date(nowMs).toISOString(),
|
|
880
|
+
summary: summarize(tasks, missions, worktrees, agents),
|
|
881
|
+
os: osState,
|
|
882
|
+
next_action: nextAction(tasks, missions, worktrees, agents, osState),
|
|
883
|
+
agents,
|
|
884
|
+
tasks,
|
|
885
|
+
missions,
|
|
886
|
+
worktrees,
|
|
887
|
+
};
|
|
835
888
|
}
|
|
836
889
|
|
|
837
890
|
function renderRadar(data) {
|
|
838
891
|
const lines = [];
|
|
839
892
|
const s = data.summary;
|
|
840
893
|
lines.push('Operator radar');
|
|
894
|
+
lines.push(`Scope: ${data.scope || 'workspace'}${data.scope === 'global' ? ' (machine)' : ' (this workspace)'}`);
|
|
841
895
|
lines.push('');
|
|
842
896
|
lines.push(`Agents: ${s.agents.active}/${s.agents.total} active`);
|
|
843
897
|
lines.push(`Tasks: ${s.tasks.open} open, ${s.tasks.claimed} claimed, ${s.tasks.review} review (${s.tasks.certifiedReview} certified)`);
|
|
@@ -1178,17 +1232,19 @@ function renderAgentTop(data) {
|
|
|
1178
1232
|
|
|
1179
1233
|
function radarCommand(args = [], options = {}) {
|
|
1180
1234
|
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
1181
|
-
console.log('Usage: atris radar [--json] [--agents]');
|
|
1235
|
+
console.log('Usage: atris radar [--json] [--agents] [--global]');
|
|
1182
1236
|
console.log('Usage: atris radar agents');
|
|
1183
1237
|
console.log('Usage: atris ctop');
|
|
1184
1238
|
console.log('');
|
|
1185
1239
|
console.log('Shows live agent processes joined with Atris tasks, missions, and worktrees.');
|
|
1240
|
+
console.log('Default scope is this workspace. Pass --global for the whole machine.');
|
|
1186
1241
|
console.log('Use --agents or ctop for a process-first CPU/memory view.');
|
|
1187
1242
|
return 0;
|
|
1188
1243
|
}
|
|
1189
|
-
const
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1244
|
+
const scope = parseScopeFlag(args);
|
|
1245
|
+
const data = collectRadar({ ...options, scope: scope.kind });
|
|
1246
|
+
const agentsOnly = scope.args.includes('--agents') || scope.args[0] === 'agents';
|
|
1247
|
+
if (scope.args.includes('--json') || args.includes('--json')) console.log(JSON.stringify(agentsOnly ? { ...agentTopPayload(data), scope: data.scope } : data, null, 2));
|
|
1192
1248
|
else if (agentsOnly) console.log(renderAgentTop(data));
|
|
1193
1249
|
else console.log(renderRadar(data));
|
|
1194
1250
|
return 0;
|