atris 3.53.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.
Files changed (119) hide show
  1. package/FOR_AGENTS.md +7 -0
  2. package/README.md +1 -1
  3. package/atris/policies/ANTISLOP.md +2 -1
  4. package/atris/skills/copy-editor/SKILL.md +2 -1
  5. package/atris/skills/design/SKILL.md +1 -1
  6. package/atris/skills/x-search/SKILL.md +24 -22
  7. package/atris/skills/youtube/SKILL.md +82 -33
  8. package/atris/team/_template/SOUL.md +40 -0
  9. package/atris/team/validator/MEMBER.md +1 -2
  10. package/bin/atris +21 -0
  11. package/bin/atris.js +563 -373
  12. package/commands/api-key.js +170 -0
  13. package/commands/auth.js +453 -43
  14. package/commands/autoland.js +3 -1
  15. package/commands/autopilot-front.js +58 -11
  16. package/commands/avail.js +5 -4
  17. package/commands/balance.js +55 -0
  18. package/commands/bench.js +29 -1
  19. package/commands/brain.js +63 -12
  20. package/commands/brainstorm.js +18 -0
  21. package/commands/business.js +149 -1
  22. package/commands/chat-scan.js +17 -4
  23. package/commands/close.js +5 -1
  24. package/commands/doctor.js +174 -0
  25. package/commands/dream.js +7 -0
  26. package/commands/drill.js +34 -2
  27. package/commands/engine.js +47 -35
  28. package/commands/errors.js +58 -8
  29. package/commands/experiments.js +14 -0
  30. package/commands/feedback.js +61 -2
  31. package/commands/fleet-report.js +27 -7
  32. package/commands/fleet.js +98 -11
  33. package/commands/founder.js +60 -21
  34. package/commands/human-missions.js +79 -4
  35. package/commands/improve.js +54 -4
  36. package/commands/init.js +68 -92
  37. package/commands/install.js +40 -0
  38. package/commands/integrations.js +399 -30
  39. package/commands/interview.js +8 -0
  40. package/commands/land.js +48 -17
  41. package/commands/learn.js +29 -4
  42. package/commands/log.js +51 -0
  43. package/commands/member.js +68 -10
  44. package/commands/mission.js +287 -50
  45. package/commands/now.js +15 -3
  46. package/commands/orb.js +97 -2
  47. package/commands/pack.js +83 -9
  48. package/commands/playbook.js +381 -0
  49. package/commands/plugin.js +16 -0
  50. package/commands/radar.js +31 -7
  51. package/commands/recap.js +128 -21
  52. package/commands/release.js +102 -2
  53. package/commands/review.js +5 -10
  54. package/commands/revisions.js +55 -0
  55. package/commands/run-front.js +12 -4
  56. package/commands/scout.js +12 -1
  57. package/commands/search.js +123 -28
  58. package/commands/sign.js +2 -2
  59. package/commands/signup.js +12 -3
  60. package/commands/site-deploy.js +17 -4
  61. package/commands/site.js +2 -2
  62. package/commands/skill-eval.js +289 -0
  63. package/commands/skill.js +56 -6
  64. package/commands/social.js +603 -0
  65. package/commands/spaceship.js +69 -6
  66. package/commands/stream.js +8 -1
  67. package/commands/study.js +32 -19
  68. package/commands/sync.js +28 -22
  69. package/commands/task.js +418 -60
  70. package/commands/teach.js +72 -63
  71. package/commands/terminal.js +25 -9
  72. package/commands/topup.js +88 -0
  73. package/commands/truth.js +52 -6
  74. package/commands/usage.js +91 -0
  75. package/commands/verify.js +3 -2
  76. package/commands/version.js +3 -1
  77. package/commands/voice.js +5 -1
  78. package/commands/who.js +69 -11
  79. package/commands/wiki.js +7 -4
  80. package/commands/wish.js +76 -5
  81. package/commands/workflow.js +327 -145
  82. package/commands/worktree.js +1 -0
  83. package/commands/write.js +5 -0
  84. package/commands/x-search.js +340 -0
  85. package/commands/xp.js +5 -1
  86. package/commands/youtube.js +551 -25
  87. package/lib/account-bound.js +47 -0
  88. package/lib/auto-accept-certified.js +8 -1
  89. package/lib/auto-lessons.js +186 -0
  90. package/lib/autoland.js +4 -2
  91. package/lib/chat-log-scan.js +7 -4
  92. package/lib/cli-json.js +77 -0
  93. package/lib/cli-scope.js +26 -0
  94. package/lib/codex-flight.js +1 -0
  95. package/lib/conductor-artifacts.js +1 -1
  96. package/lib/context-gatherer.js +11 -0
  97. package/lib/developer-api.js +116 -0
  98. package/lib/feedback-triage.js +274 -0
  99. package/lib/first-minute.js +398 -0
  100. package/lib/fleet.js +115 -34
  101. package/lib/functional-owner.js +22 -0
  102. package/lib/known-commands.js +31 -5
  103. package/lib/member-scaffold.js +197 -0
  104. package/lib/mission-root.js +4 -2
  105. package/lib/noninteractive.js +88 -0
  106. package/lib/policy-lessons.js +4 -1
  107. package/lib/revision-metric.js +279 -0
  108. package/lib/scratch-root.js +48 -0
  109. package/lib/skill-eval-gate.js +249 -0
  110. package/lib/task-db.js +4 -0
  111. package/lib/task-proof.js +69 -8
  112. package/lib/verifier-quality.js +69 -0
  113. package/lib/wish-audit.js +2 -2
  114. package/lib/wish-delegate.js +10 -1
  115. package/lib/workspace-scaffold.js +270 -0
  116. package/package.json +3 -2
  117. package/scripts/det/checklist-score.js +191 -0
  118. package/scripts/det/ytsearch +31 -0
  119. 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.status !== 402) throw error;
776
- console.error('this pack is paid. buy it on its page, then run this again.');
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 (response.status < 200 || response.status >= 300) {
2738
- if (origin.type === 'registry' && response.status === 404) {
2806
+ if (origin.type === 'registry') {
2807
+ if (response.status === 404) {
2739
2808
  throw registryNotFoundError(origin.slug, authenticated);
2740
2809
  }
2741
- if (origin.type === 'registry' && response.status === 402) {
2742
- console.error('this pack is paid. buy it on its page, then run this again.');
2743
- console.error(registryUrl(`/packs/${encodeURIComponent(origin.slug)}`, deps));
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 };
@@ -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);