atris 3.53.0 → 3.56.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/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +82 -33
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/bin/atris +21 -0
- package/bin/atris.js +563 -373
- 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/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 +565 -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 +287 -50
- package/commands/now.js +15 -3
- 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 +31 -7
- 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 +435 -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 -25
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +8 -1
- 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/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-root.js +4 -2
- package/lib/noninteractive.js +88 -0
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +4 -0
- package/lib/task-proof.js +69 -8
- 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 +3 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/utils/update-check.js +15 -0
package/commands/orb.js
CHANGED
|
@@ -86,10 +86,68 @@ function renderHint() {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function showHelp() {
|
|
89
|
-
console.log('Usage: atris orb [--once] [--engine claude|codex|fast]');
|
|
89
|
+
console.log('Usage: atris orb [--once] [--json] [--engine claude|codex|fast]');
|
|
90
|
+
console.log(' atris orb <n> | atris orb --pick <n>');
|
|
90
91
|
console.log(' atris orb scorecard [--days N]');
|
|
91
92
|
console.log('');
|
|
92
93
|
console.log('Choose a next move while background engine jobs keep working.');
|
|
94
|
+
console.log('--json emits {moves:[{n,label,command}]} and exits.');
|
|
95
|
+
console.log('Supported flags: --once, --json, --pick, --engine, --help, -h');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function suggestionCommand(suggestion, n) {
|
|
99
|
+
const kind = String(suggestion && suggestion.kind || '');
|
|
100
|
+
const label = String(suggestion && suggestion.label || '');
|
|
101
|
+
if (kind === 'digest') return 'atris stream --once';
|
|
102
|
+
if (kind === 'log') return 'atris log';
|
|
103
|
+
if (kind === 'review' && /^accept\b/i.test(label)) return 'atris task reviews';
|
|
104
|
+
return `atris orb --pick ${n}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function movesPayload(suggestions) {
|
|
108
|
+
return {
|
|
109
|
+
moves: suggestions.map((suggestion, index) => {
|
|
110
|
+
const n = index + 1;
|
|
111
|
+
return {
|
|
112
|
+
n,
|
|
113
|
+
label: suggestion.label || '',
|
|
114
|
+
command: suggestionCommand(suggestion, n),
|
|
115
|
+
};
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parsePick(args = []) {
|
|
121
|
+
const equals = args.find((arg) => arg.startsWith('--pick='));
|
|
122
|
+
if (equals) {
|
|
123
|
+
const value = Number(equals.slice('--pick='.length));
|
|
124
|
+
return Number.isInteger(value) && value >= 1 ? value : null;
|
|
125
|
+
}
|
|
126
|
+
const idx = args.indexOf('--pick');
|
|
127
|
+
if (idx !== -1) {
|
|
128
|
+
const value = Number(args[idx + 1]);
|
|
129
|
+
return Number.isInteger(value) && value >= 1 ? value : null;
|
|
130
|
+
}
|
|
131
|
+
const positional = args.find((arg) => /^[1-9]$/.test(String(arg)));
|
|
132
|
+
if (positional) return Number(positional);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function unknownOrbFlags(args = []) {
|
|
137
|
+
const known = new Set(['--once', '--json', '--help', '-h', '--engine', '--pick']);
|
|
138
|
+
const unknown = [];
|
|
139
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
140
|
+
const arg = String(args[i] || '');
|
|
141
|
+
if (!arg.startsWith('-')) continue;
|
|
142
|
+
if (arg.startsWith('--engine=')) continue;
|
|
143
|
+
if (arg.startsWith('--pick=')) continue;
|
|
144
|
+
if (arg === '--engine' || arg === '--pick') {
|
|
145
|
+
i += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!known.has(arg)) unknown.push(arg);
|
|
149
|
+
}
|
|
150
|
+
return unknown;
|
|
93
151
|
}
|
|
94
152
|
|
|
95
153
|
function tailLines(text, count = 60) {
|
|
@@ -115,20 +173,57 @@ async function runOrb(args = []) {
|
|
|
115
173
|
return 0;
|
|
116
174
|
}
|
|
117
175
|
|
|
176
|
+
const unknown = unknownOrbFlags(args);
|
|
177
|
+
if (unknown.length) {
|
|
178
|
+
console.error(`unknown orb option: ${unknown.join(', ')}`);
|
|
179
|
+
console.error('supported flags: --once, --json, --pick, --engine, --help, -h');
|
|
180
|
+
return 2;
|
|
181
|
+
}
|
|
182
|
+
|
|
118
183
|
const engine = parseEngine(args);
|
|
119
184
|
if (!VALID_ENGINES.has(engine)) {
|
|
120
185
|
console.error(`Unknown orb engine: ${engine || '(empty)'}. Use claude, codex, or fast.`);
|
|
121
186
|
return 2;
|
|
122
187
|
}
|
|
123
188
|
|
|
189
|
+
const asJson = args.includes('--json');
|
|
190
|
+
const pick = parsePick(args);
|
|
124
191
|
const engineCommand = engineInvocation(engine, '').command;
|
|
125
|
-
if (!findExecutableOnPath(engineCommand)) {
|
|
192
|
+
if (!asJson && !findExecutableOnPath(engineCommand)) {
|
|
126
193
|
console.error(`orb warning: engine binary "${engineCommand}" is missing from PATH; picks will fail until it is installed.`);
|
|
127
194
|
}
|
|
128
195
|
|
|
129
196
|
const root = workspaceRoot();
|
|
130
197
|
let context = await collectOrbContext(root, { threads: [] });
|
|
198
|
+
|
|
199
|
+
if (asJson) {
|
|
200
|
+
const suggestions = (context.suggestions || []).slice(0, MAX_VISIBLE_SUGGESTIONS);
|
|
201
|
+
console.log(JSON.stringify(movesPayload(suggestions), null, 2));
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
131
205
|
let visibleSuggestions = renderContext(context);
|
|
206
|
+
|
|
207
|
+
if (pick != null) {
|
|
208
|
+
const choice = visibleSuggestions[pick - 1];
|
|
209
|
+
if (!choice) {
|
|
210
|
+
console.error(`orb: no move ${pick}. Pick 1-${visibleSuggestions.length || 0}.`);
|
|
211
|
+
return 2;
|
|
212
|
+
}
|
|
213
|
+
// One-shot pick: record the choice and print the runnable command, then exit.
|
|
214
|
+
// Interactive mode still launches background engines; agents use --json + --pick.
|
|
215
|
+
const appended = await appendOrbChoice(root, cleanLabel(choice.label));
|
|
216
|
+
if (!appended.ok) {
|
|
217
|
+
console.error(`Could not update now.md: ${appended.error}`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
const command = suggestionCommand(choice, pick);
|
|
221
|
+
console.log(`picked ${pick}: ${choice.label}`);
|
|
222
|
+
console.log(`next: ${command}`);
|
|
223
|
+
if (choice.prompt) console.log(`prompt: ${choice.prompt}`);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
132
227
|
const once = args.includes('--once') || !process.stdin.isTTY || !process.stdout.isTTY;
|
|
133
228
|
if (once) return 0;
|
|
134
229
|
|
package/commands/pack.js
CHANGED
|
@@ -745,6 +745,74 @@ async function postPackToRegistry(manifest, zipBuffer, deps = {}) {
|
|
|
745
745
|
return parsed.data;
|
|
746
746
|
}
|
|
747
747
|
|
|
748
|
+
function paidPackBarrierFromResponse(response, slug) {
|
|
749
|
+
const status = Number(response && response.status) || 0;
|
|
750
|
+
if (status === 404) return null;
|
|
751
|
+
|
|
752
|
+
const rawBody = response && response.body;
|
|
753
|
+
const body = !rawBody
|
|
754
|
+
? Buffer.alloc(0)
|
|
755
|
+
: (Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody)));
|
|
756
|
+
const empty = body.length === 0;
|
|
757
|
+
const parsed = parseJsonBody(body);
|
|
758
|
+
const data = parsed.data && typeof parsed.data === 'object' && !Array.isArray(parsed.data)
|
|
759
|
+
? parsed.data
|
|
760
|
+
: {};
|
|
761
|
+
const priceRaw = data.price_cents ?? data.priceCents;
|
|
762
|
+
const priceCents = Number(priceRaw);
|
|
763
|
+
const hasPrice = Number.isFinite(priceCents) && priceCents > 0;
|
|
764
|
+
const nameSource = [data.name, data.title, data.slug, slug]
|
|
765
|
+
.find((value) => typeof value === 'string' && value.trim());
|
|
766
|
+
const details = {
|
|
767
|
+
name: String(nameSource || slug || 'this pack').trim().toLowerCase(),
|
|
768
|
+
priceCents: hasPrice ? Math.round(priceCents) : null,
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const entitled = data.entitled === true
|
|
772
|
+
|| data.has_access === true
|
|
773
|
+
|| data.hasAccess === true
|
|
774
|
+
|| data.is_owner === true
|
|
775
|
+
|| data.isOwner === true;
|
|
776
|
+
const unentitled = data.entitled === false
|
|
777
|
+
|| data.has_access === false
|
|
778
|
+
|| data.hasAccess === false
|
|
779
|
+
|| data.entitlement === false
|
|
780
|
+
|| data.entitlement === null;
|
|
781
|
+
const message = String(data.error || data.message || data.detail || '');
|
|
782
|
+
const looksLikeZip = body.length >= 4 && body[0] === 0x50 && body[1] === 0x4b;
|
|
783
|
+
const paid = status === 402
|
|
784
|
+
|| data.paid === true
|
|
785
|
+
|| data.is_paid === true
|
|
786
|
+
|| data.isPaid === true
|
|
787
|
+
|| data.purchase_required === true
|
|
788
|
+
|| typeof data.checkout_hint === 'string'
|
|
789
|
+
|| /purchase required/i.test(message)
|
|
790
|
+
|| (hasPrice && (unentitled || !looksLikeZip));
|
|
791
|
+
|
|
792
|
+
if (entitled && status >= 200 && status < 300 && looksLikeZip) return null;
|
|
793
|
+
if (status === 402 || status === 403) return details;
|
|
794
|
+
if (status >= 200 && status < 300 && empty) return details;
|
|
795
|
+
if (paid && !entitled && !looksLikeZip) return details;
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function printPaidPackBarrier(slug, details = {}, deps = {}) {
|
|
800
|
+
const name = String(details.name || slug || 'this pack').trim().toLowerCase() || 'this pack';
|
|
801
|
+
const cents = Number(details.priceCents);
|
|
802
|
+
const price = Number.isFinite(cents) && cents > 0 ? ` (${formatSalesDollars(cents)})` : '';
|
|
803
|
+
console.error(`this pack is paid: ${name}${price}.`);
|
|
804
|
+
console.error('buy it on its page, then run this again.');
|
|
805
|
+
console.error(registryUrl(`/packs/${encodeURIComponent(slug)}`, deps));
|
|
806
|
+
console.error('already bought it? run atris login');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function throwPaidPackBarrier(barrier, status) {
|
|
810
|
+
const error = new Error('this pack is paid');
|
|
811
|
+
error.status = status;
|
|
812
|
+
error.paidPack = barrier;
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
|
|
748
816
|
async function fetchRegistryZip(slug, deps = {}) {
|
|
749
817
|
const url = registryUrl(`/api/pack/registry/${encodeURIComponent(slug)}`, deps);
|
|
750
818
|
let result;
|
|
@@ -756,8 +824,10 @@ async function fetchRegistryZip(slug, deps = {}) {
|
|
|
756
824
|
throw new Error('could not reach pack registry. check your connection and try again.');
|
|
757
825
|
}
|
|
758
826
|
const { response, authenticated } = result;
|
|
827
|
+
if (response.status === 404) throw registryNotFoundError(slug, authenticated);
|
|
828
|
+
const barrier = paidPackBarrierFromResponse(response, slug);
|
|
829
|
+
if (barrier) throwPaidPackBarrier(barrier, response.status);
|
|
759
830
|
if (response.status < 200 || response.status >= 300) {
|
|
760
|
-
if (response.status === 404) throw registryNotFoundError(slug, authenticated);
|
|
761
831
|
const error = new Error(responseErrorText(response, `registry lookup failed for ${slug} with status ${response.status}`));
|
|
762
832
|
error.status = response.status;
|
|
763
833
|
throw error;
|
|
@@ -772,9 +842,8 @@ async function fetchRegistryZipForUser(slug, deps = {}) {
|
|
|
772
842
|
try {
|
|
773
843
|
return await fetchRegistryZip(slug, deps);
|
|
774
844
|
} catch (error) {
|
|
775
|
-
if (error
|
|
776
|
-
|
|
777
|
-
console.error(registryUrl(`/packs/${encodeURIComponent(slug)}`, deps));
|
|
845
|
+
if (!error || !error.paidPack) throw error;
|
|
846
|
+
printPaidPackBarrier(slug, error.paidPack, deps);
|
|
778
847
|
return null;
|
|
779
848
|
}
|
|
780
849
|
}
|
|
@@ -2734,17 +2803,22 @@ async function updatePack(rawArgs, cwd = process.cwd(), options = {}) {
|
|
|
2734
2803
|
const headers = credentials && credentials.token ? { Authorization: `Bearer ${credentials.token}` } : {};
|
|
2735
2804
|
response = await request(url, { method: 'GET', headers });
|
|
2736
2805
|
}
|
|
2737
|
-
if (
|
|
2738
|
-
if (
|
|
2806
|
+
if (origin.type === 'registry') {
|
|
2807
|
+
if (response.status === 404) {
|
|
2739
2808
|
throw registryNotFoundError(origin.slug, authenticated);
|
|
2740
2809
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2810
|
+
const barrier = paidPackBarrierFromResponse(response, origin.slug);
|
|
2811
|
+
if (barrier) {
|
|
2812
|
+
printPaidPackBarrier(origin.slug, barrier, deps);
|
|
2744
2813
|
return 1;
|
|
2745
2814
|
}
|
|
2815
|
+
}
|
|
2816
|
+
if (response.status < 200 || response.status >= 300) {
|
|
2746
2817
|
throw new Error(`download failed with status ${response.status}`);
|
|
2747
2818
|
}
|
|
2819
|
+
if (!response.body || response.body.length === 0) {
|
|
2820
|
+
throw new Error('download returned an empty zip');
|
|
2821
|
+
}
|
|
2748
2822
|
|
|
2749
2823
|
const entries = readZipBuffer(response.body);
|
|
2750
2824
|
return stagePackUpdate({
|
|
@@ -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);
|