@pilllesss/yorn 1.0.181 → 1.0.183

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.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (47) hide show
  1. package/README.md +19 -0
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/skills/subagent/SKILL.md +45 -4
  45. package/dist/skills/subagent/run_agent.sh +155 -0
  46. package/dist/yorn.cjs +621 -621
  47. package/package.json +2 -2
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for pr-analyzer.py (stdlib unittest, no extra deps)."""
3
+
4
+ import importlib.util
5
+ import os
6
+ import unittest
7
+
8
+ # The script has a hyphen in its name, so load it by path.
9
+ _HERE = os.path.dirname(os.path.abspath(__file__))
10
+ _spec = importlib.util.spec_from_file_location(
11
+ 'pr_analyzer', os.path.join(_HERE, 'pr-analyzer.py')
12
+ )
13
+ pr_analyzer = importlib.util.module_from_spec(_spec)
14
+ _spec.loader.exec_module(pr_analyzer)
15
+
16
+ # Convenient aliases
17
+ FileStats = pr_analyzer.FileStats
18
+
19
+
20
+ # ═══════════════════════════════════════════════════════════════
21
+ # parse_diff — filename extraction (existing tests)
22
+ # ═══════════════════════════════════════════════════════════════
23
+
24
+ class ParseDiffFilenameTest(unittest.TestCase):
25
+ def test_lib_prefixed_path(self):
26
+ # "lib/" embeds a literal "b/" that the old regex swallowed.
27
+ diff = (
28
+ "diff --git a/lib/foo.py b/lib/foo.py\n"
29
+ "index 1234567..89abcde 100644\n"
30
+ "--- a/lib/foo.py\n"
31
+ "+++ b/lib/foo.py\n"
32
+ "@@ -1,2 +1,3 @@\n"
33
+ " unchanged\n"
34
+ "+added line\n"
35
+ "-removed line\n"
36
+ )
37
+ files = pr_analyzer.parse_diff(diff)
38
+ self.assertEqual(len(files), 1)
39
+ self.assertEqual(files[0].filename, 'lib/foo.py')
40
+ self.assertEqual(files[0].additions, 1)
41
+ self.assertEqual(files[0].deletions, 1)
42
+
43
+ def test_normal_path(self):
44
+ diff = (
45
+ "diff --git a/src/main.py b/src/main.py\n"
46
+ "index 1111111..2222222 100644\n"
47
+ "--- a/src/main.py\n"
48
+ "+++ b/src/main.py\n"
49
+ "@@ -0,0 +1 @@\n"
50
+ "+print('hi')\n"
51
+ )
52
+ files = pr_analyzer.parse_diff(diff)
53
+ self.assertEqual(len(files), 1)
54
+ self.assertEqual(files[0].filename, 'src/main.py')
55
+
56
+ def test_other_embedded_b_slash_prefixes(self):
57
+ # web/ and db/ also contain a literal "b/".
58
+ diff = (
59
+ "diff --git a/web/x.js b/web/x.js\n"
60
+ "+++ b/web/x.js\n"
61
+ "+console.log(1)\n"
62
+ "diff --git a/db/y.sql b/db/y.sql\n"
63
+ "+++ b/db/y.sql\n"
64
+ "+SELECT 1;\n"
65
+ )
66
+ files = pr_analyzer.parse_diff(diff)
67
+ self.assertEqual([f.filename for f in files], ['web/x.js', 'db/y.sql'])
68
+
69
+ def test_rename_falls_back_to_b_side(self):
70
+ diff = (
71
+ "diff --git a/old/name.py b/new/name.py\n"
72
+ "similarity index 100%\n"
73
+ "rename from old/name.py\n"
74
+ "rename to new/name.py\n"
75
+ )
76
+ files = pr_analyzer.parse_diff(diff)
77
+ self.assertEqual(len(files), 1)
78
+ self.assertEqual(files[0].filename, 'new/name.py')
79
+
80
+
81
+ # ═══════════════════════════════════════════════════════════════
82
+ # detect_language
83
+ # ═══════════════════════════════════════════════════════════════
84
+
85
+ class DetectLanguageTest(unittest.TestCase):
86
+ def test_common_extensions(self):
87
+ cases = {
88
+ 'app.py': 'Python',
89
+ 'index.ts': 'TypeScript',
90
+ 'main.rs': 'Rust',
91
+ 'handler.go': 'Go',
92
+ 'App.java': 'Java',
93
+ 'Activity.kt': 'Kotlin',
94
+ 'ViewController.swift': 'Swift',
95
+ 'app.tsx': 'TypeScript/React',
96
+ 'style.css': 'CSS',
97
+ 'query.sql': 'SQL',
98
+ }
99
+ for filename, expected in cases.items():
100
+ with self.subTest(filename=filename):
101
+ self.assertEqual(pr_analyzer.detect_language(filename), expected)
102
+
103
+ def test_unknown_extension(self):
104
+ self.assertEqual(pr_analyzer.detect_language('data.xyz'), 'unknown')
105
+ self.assertEqual(pr_analyzer.detect_language('Makefile'), 'unknown')
106
+
107
+ def test_cpp_variants(self):
108
+ for ext in ('.cpp', '.hpp', '.cc', '.cxx', '.hh', '.hxx'):
109
+ with self.subTest(ext=ext):
110
+ self.assertEqual(pr_analyzer.detect_language(f'file{ext}'), 'C++')
111
+
112
+
113
+ # ═══════════════════════════════════════════════════════════════
114
+ # is_test_file
115
+ # ═══════════════════════════════════════════════════════════════
116
+
117
+ class IsTestFileTest(unittest.TestCase):
118
+ def test_python_test_prefix(self):
119
+ self.assertTrue(pr_analyzer.is_test_file('tests/test_handler.py'))
120
+ self.assertTrue(pr_analyzer.is_test_file('test_utils.py'))
121
+
122
+ def test_python_test_suffix(self):
123
+ self.assertTrue(pr_analyzer.is_test_file('handler_test.py'))
124
+
125
+ def test_rust_test_suffix(self):
126
+ self.assertTrue(pr_analyzer.is_test_file('src/my_module_test.rs'))
127
+ self.assertTrue(pr_analyzer.is_test_file('parser_test.rs'))
128
+
129
+ def test_go_test_suffix(self):
130
+ self.assertTrue(pr_analyzer.is_test_file('handler_test.go'))
131
+ self.assertTrue(pr_analyzer.is_test_file('pkg/auth_test.go'))
132
+
133
+ def test_js_ts_test_and_spec(self):
134
+ self.assertTrue(pr_analyzer.is_test_file('handler.test.ts'))
135
+ self.assertTrue(pr_analyzer.is_test_file('utils.spec.js'))
136
+ self.assertTrue(pr_analyzer.is_test_file('App.test.tsx'))
137
+
138
+ def test_tests_directory(self):
139
+ self.assertTrue(pr_analyzer.is_test_file('tests/conftest.py'))
140
+ self.assertTrue(pr_analyzer.is_test_file('test/helpers.js'))
141
+
142
+ def test_dunder_tests_directory(self):
143
+ self.assertTrue(pr_analyzer.is_test_file('__tests__/Button.test.tsx'))
144
+
145
+ def test_non_test_files_rejected(self):
146
+ self.assertFalse(pr_analyzer.is_test_file('handler.go'))
147
+ self.assertFalse(pr_analyzer.is_test_file('module.rs'))
148
+ self.assertFalse(pr_analyzer.is_test_file('src/utils.py'))
149
+ self.assertFalse(pr_analyzer.is_test_file('lib/parser.js'))
150
+ self.assertFalse(pr_analyzer.is_test_file('contest.py'))
151
+
152
+ def test_test_substring_not_matched(self):
153
+ """Files containing 'test_' as substring must NOT be flagged."""
154
+ self.assertFalse(pr_analyzer.is_test_file('latest_report.py'))
155
+ self.assertFalse(pr_analyzer.is_test_file('contest_utils.py'))
156
+ self.assertFalse(pr_analyzer.is_test_file('src/latest_handler.py'))
157
+
158
+
159
+ # ═══════════════════════════════════════════════════════════════
160
+ # is_config_file
161
+ # ═══════════════════════════════════════════════════════════════
162
+
163
+ class IsConfigFileTest(unittest.TestCase):
164
+ def test_known_json_configs(self):
165
+ self.assertTrue(pr_analyzer.is_config_file('package.json'))
166
+ self.assertTrue(pr_analyzer.is_config_file('tsconfig.json'))
167
+ self.assertTrue(pr_analyzer.is_config_file('.eslintrc.json'))
168
+
169
+ def test_known_yaml_configs(self):
170
+ self.assertTrue(pr_analyzer.is_config_file('docker-compose.yml'))
171
+ self.assertTrue(pr_analyzer.is_config_file('.github/workflows/ci.yml'))
172
+ self.assertTrue(pr_analyzer.is_config_file('.prettierrc.yml'))
173
+
174
+ def test_known_toml_configs(self):
175
+ self.assertTrue(pr_analyzer.is_config_file('Cargo.toml'))
176
+ self.assertTrue(pr_analyzer.is_config_file('pyproject.toml'))
177
+
178
+ def test_env_files(self):
179
+ self.assertTrue(pr_analyzer.is_config_file('.env'))
180
+ self.assertTrue(pr_analyzer.is_config_file('.env.local'))
181
+ self.assertTrue(pr_analyzer.is_config_file('.env.production'))
182
+
183
+ def test_config_in_filename(self):
184
+ self.assertTrue(pr_analyzer.is_config_file('app.config.ts'))
185
+ self.assertTrue(pr_analyzer.is_config_file('database_config.yml'))
186
+
187
+ def test_data_files_rejected(self):
188
+ """Data files must NOT be flagged as config."""
189
+ self.assertFalse(pr_analyzer.is_config_file('data.json'))
190
+ self.assertFalse(pr_analyzer.is_config_file('openapi.yaml'))
191
+ self.assertFalse(pr_analyzer.is_config_file('swagger.json'))
192
+ self.assertFalse(pr_analyzer.is_config_file('fixtures/sample.yml'))
193
+ self.assertFalse(pr_analyzer.is_config_file('translations.json'))
194
+ self.assertFalse(pr_analyzer.is_config_file('schema.toml'))
195
+
196
+ def test_config_directory(self):
197
+ self.assertTrue(pr_analyzer.is_config_file('config/settings.yaml'))
198
+ self.assertTrue(pr_analyzer.is_config_file('config/database.yml'))
199
+ self.assertTrue(pr_analyzer.is_config_file('src/config/settings.json'))
200
+
201
+ def test_source_files_rejected(self):
202
+ self.assertFalse(pr_analyzer.is_config_file('src/index.ts'))
203
+ self.assertFalse(pr_analyzer.is_config_file('lib/utils.py'))
204
+
205
+
206
+ # ═══════════════════════════════════════════════════════════════
207
+ # calculate_complexity
208
+ # ═══════════════════════════════════════════════════════════════
209
+
210
+ class CalculateComplexityTest(unittest.TestCase):
211
+ def test_empty_files(self):
212
+ self.assertEqual(pr_analyzer.calculate_complexity([]), 0.0)
213
+
214
+ def test_small_simple_change(self):
215
+ files = [FileStats(filename='app.py', additions=5, deletions=2,
216
+ language='Python')]
217
+ score = pr_analyzer.calculate_complexity(files)
218
+ self.assertLess(score, 0.3)
219
+
220
+ def test_large_multi_language_change(self):
221
+ files = [
222
+ FileStats(filename='app.py', additions=300, deletions=100, language='Python'),
223
+ FileStats(filename='main.rs', additions=200, deletions=50, language='Rust'),
224
+ FileStats(filename='index.ts', additions=150, deletions=80, language='TypeScript'),
225
+ FileStats(filename='handler.go', additions=100, deletions=30, language='Go'),
226
+ FileStats(filename='App.tsx', additions=50, deletions=20, language='TypeScript/React'),
227
+ ]
228
+ score = pr_analyzer.calculate_complexity(files)
229
+ self.assertGreater(score, 0.5)
230
+
231
+ def test_test_heavy_change_is_lower(self):
232
+ """Changes with high test ratio should have lower complexity."""
233
+ prod_files = [FileStats(filename='app.py', additions=100, deletions=50,
234
+ language='Python')]
235
+ test_files = [
236
+ FileStats(filename='app.py', additions=100, deletions=50,
237
+ language='Python'),
238
+ FileStats(filename='tests/test_app.py', additions=100, deletions=0,
239
+ language='Python', is_test=True),
240
+ ]
241
+ score_prod = pr_analyzer.calculate_complexity(prod_files)
242
+ score_test = pr_analyzer.calculate_complexity(test_files)
243
+ self.assertLess(score_test, score_prod)
244
+
245
+
246
+ # ═══════════════════════════════════════════════════════════════
247
+ # identify_risk_factors
248
+ # ═══════════════════════════════════════════════════════════════
249
+
250
+ class IdentifyRiskFactorsTest(unittest.TestCase):
251
+ def test_large_pr_flagged(self):
252
+ files = [FileStats(filename='big.py', additions=300, deletions=200,
253
+ language='Python')]
254
+ risks = pr_analyzer.identify_risk_factors(files)
255
+ self.assertTrue(any('Large PR' in r for r in risks))
256
+
257
+ def test_no_tests_flagged(self):
258
+ files = [FileStats(filename='app.py', additions=40, deletions=20,
259
+ language='Python')]
260
+ risks = pr_analyzer.identify_risk_factors(files)
261
+ self.assertTrue(any(pr_analyzer.RISK_NO_TESTS in r for r in risks))
262
+
263
+ def test_with_tests_not_flagged(self):
264
+ files = [
265
+ FileStats(filename='app.py', additions=40, deletions=20,
266
+ language='Python'),
267
+ FileStats(filename='tests/test_app.py', additions=30, deletions=0,
268
+ language='Python', is_test=True),
269
+ ]
270
+ risks = pr_analyzer.identify_risk_factors(files)
271
+ self.assertFalse(any(pr_analyzer.RISK_NO_TESTS in r for r in risks))
272
+
273
+ def test_security_sensitive_file(self):
274
+ files = [FileStats(filename='src/auth/login.py', additions=10, deletions=5,
275
+ language='Python')]
276
+ risks = pr_analyzer.identify_risk_factors(files)
277
+ self.assertTrue(any('Security-sensitive' in r for r in risks))
278
+
279
+ def test_database_migration(self):
280
+ files = [FileStats(filename='migrations/001_init.sql', additions=20, deletions=0,
281
+ language='SQL')]
282
+ risks = pr_analyzer.identify_risk_factors(files)
283
+ self.assertTrue(any('Database' in r for r in risks))
284
+
285
+ def test_test_substring_files_still_flag_no_tests(self):
286
+ """Files like latest_report.py must not suppress NO_TEST_CHANGES."""
287
+ files = [
288
+ FileStats(filename='latest_report.py', additions=40, deletions=20,
289
+ language='Python'),
290
+ FileStats(filename='contest_utils.py', additions=30, deletions=10,
291
+ language='Python'),
292
+ ]
293
+ risks = pr_analyzer.identify_risk_factors(files)
294
+ self.assertTrue(any(pr_analyzer.RISK_NO_TESTS in r for r in risks))
295
+
296
+
297
+ # ═══════════════════════════════════════════════════════════════
298
+ # generate_suggestions
299
+ # ═══════════════════════════════════════════════════════════════
300
+
301
+ class GenerateSuggestionsTest(unittest.TestCase):
302
+ def test_returns_list(self):
303
+ files = [FileStats(filename='app.py', additions=10, deletions=5,
304
+ language='Python')]
305
+ result = pr_analyzer.generate_suggestions(files, 0.1, [])
306
+ self.assertIsInstance(result, list)
307
+ self.assertGreater(len(result), 0)
308
+
309
+ def test_large_pr_split_suggestion(self):
310
+ files = [FileStats(filename='big.py', additions=600, deletions=300,
311
+ language='Python')]
312
+ result = pr_analyzer.generate_suggestions(files, 0.3, [])
313
+ self.assertTrue(any('splitting' in s.lower() for s in result))
314
+
315
+ def test_no_tests_suggestion(self):
316
+ files = [FileStats(filename='app.py', additions=40, deletions=20,
317
+ language='Python')]
318
+ risks = [f"{pr_analyzer.RISK_NO_TESTS}: no tests"]
319
+ result = pr_analyzer.generate_suggestions(files, 0.2, risks)
320
+ self.assertTrue(any('test' in s.lower() for s in result))
321
+
322
+ def test_rust_suggestion(self):
323
+ files = [FileStats(filename='lib.rs', additions=10, deletions=5,
324
+ language='Rust')]
325
+ result = pr_analyzer.generate_suggestions(files, 0.1, [])
326
+ self.assertTrue(any('unwrap' in s.lower() for s in result))
327
+
328
+
329
+ # ═══════════════════════════════════════════════════════════════
330
+ # analyze_pr — end-to-end integration
331
+ # ═══════════════════════════════════════════════════════════════
332
+
333
+ class AnalyzePRTest(unittest.TestCase):
334
+ def test_end_to_end(self):
335
+ diff = (
336
+ "diff --git a/src/app.py b/src/app.py\n"
337
+ "index 1111111..2222222 100644\n"
338
+ "--- a/src/app.py\n"
339
+ "+++ b/src/app.py\n"
340
+ "@@ -1,3 +1,5 @@\n"
341
+ " import os\n"
342
+ "+import sys\n"
343
+ "+import json\n"
344
+ " def main():\n"
345
+ "+ print('hello')\n"
346
+ "+ return 0\n"
347
+ "diff --git a/tests/test_app.py b/tests/test_app.py\n"
348
+ "new file mode 100644\n"
349
+ "--- /dev/null\n"
350
+ "+++ b/tests/test_app.py\n"
351
+ "@@ -0,0 +1,3 @@\n"
352
+ "+from app import main\n"
353
+ "+def test_main():\n"
354
+ "+ assert main() == 0\n"
355
+ )
356
+ analysis = pr_analyzer.analyze_pr(diff)
357
+
358
+ self.assertEqual(analysis.total_files, 2)
359
+ self.assertEqual(analysis.total_additions, 7) # 4 + 3
360
+ self.assertEqual(analysis.total_deletions, 0)
361
+ self.assertGreater(len(analysis.suggestions), 0)
362
+ self.assertIn('XS (Extra Small)', analysis.size_category)
363
+
364
+ # Verify test file was detected
365
+ test_file = [f for f in analysis.files if 'test' in f.filename][0]
366
+ self.assertTrue(test_file.is_test)
367
+
368
+ # Verify no "NO_TEST_CHANGES" risk since tests are present
369
+ self.assertFalse(
370
+ any(pr_analyzer.RISK_NO_TESTS in r for r in analysis.risk_factors)
371
+ )
372
+
373
+ def test_empty_diff(self):
374
+ analysis = pr_analyzer.analyze_pr("")
375
+ self.assertEqual(analysis.total_files, 0)
376
+ self.assertEqual(analysis.complexity_score, 0.0)
377
+
378
+
379
+ if __name__ == '__main__':
380
+ unittest.main()
@@ -3,13 +3,54 @@ name: "subagent"
3
3
  description: "Delegate tasks, perform parallel research, or run focused subtasks using a stateless child Yorn agent via bash. Use when a task requires deep isolated research, executing tasks in parallel, running read-only code analysis without polluting current conversation context, or executing subtasks in a fresh unsaved session."
4
4
  ---
5
5
 
6
- # Subagent Task Delegation via Yorn CLI
6
+ # Subagent Task Delegation via Yorn CLI & Cross-Platform Shell Script
7
7
 
8
- This skill teaches how to spawn and manage child subagents using `yorn` CLI commands executed directly through the `bash` tool. Each subagent runs as an independent, stateless child session without persisting session history (`--no-session`), returning results directly to your bash output.
8
+ This skill teaches how to spawn and manage child subagents using `yorn` CLI commands or the bundled cross-platform shell script `run_agent.sh` executed directly through the `bash` tool. Each subagent runs as an independent, stateless child session without persisting session history (`--no-session`), returning results directly to your bash output.
9
+
10
+ The bundled script `run_agent.sh` automatically resolves the correct executable path and environment across **all operating systems** (Linux, macOS, Android Termux, iOS iSH/a-Shell, and Windows Git Bash/WSL/MSYS2).
11
+
12
+ ---
13
+
14
+ ## 🚀 Recommended: Using `run_agent.sh` (Cross-Platform Script)
15
+
16
+ Located in the skill directory: `<skill_dir>/run_agent.sh`
17
+
18
+ ### 1. Basic Invocation
19
+ ```bash
20
+ sh "<skill_dir>/run_agent.sh" "Your prompt here"
21
+ ```
22
+
23
+ ### 2. Specifying Model, Provider & Tools
24
+ ```bash
25
+ sh "<skill_dir>/run_agent.sh" -P openrouter -m anthropic/claude-3.5-sonnet -t read,grep,find,ls "Search for auth logic"
26
+ ```
27
+
28
+ ### 3. Pure Computation (No Tools)
29
+ ```bash
30
+ sh "<skill_dir>/run_agent.sh" -nt "Explain the time complexity of this algorithm"
31
+ ```
32
+
33
+ ### 4. Custom System Prompt & Thinking Level
34
+ ```bash
35
+ sh "<skill_dir>/run_agent.sh" -s "You are an expert security auditor." -k high "Audit endpoints in src/api/"
36
+ ```
37
+
38
+ ### 5. Script Flags Reference
39
+ | Flag | Description | Default |
40
+ |---|---|---|
41
+ | `-p, --prompt <text>` | Subagent prompt text | Positional arg or stdin |
42
+ | `-m, --model <name>` | Target model | `$YORN_MODEL` |
43
+ | `-P, --provider <name>` | Target provider | `$YORN_PROVIDER` |
44
+ | `-t, --tools <list>` | Allowlist of tools (`read,grep,find,ls`) | All default tools |
45
+ | `-nt, --no-tools` | Disable all tools (pure reasoning) | Off |
46
+ | `-s, --system-prompt <text>` | Custom system prompt | Default prompt |
47
+ | `-k, --thinking <level>` | Thinking level (`off`, `low`, `medium`, `high`, `max`) | Model default |
48
+ | `--json` | Stream structured JSON event payloads | Off |
49
+ | `-f, --file <path>` | Read prompt from file | None |
9
50
 
10
51
  ---
11
52
 
12
- ## 🚀 Core Command Patterns
53
+ ## Direct CLI Command Patterns
13
54
 
14
55
  ### 1. Direct Execution (Standard Delegation)
15
56
  Run a self-contained subtask. The result prints directly to stdout in the bash tool result:
@@ -72,7 +113,7 @@ yorn -p --no-session "Check tsconfig.json and suggest compiler options for stric
72
113
 
73
114
  ---
74
115
 
75
- ## 📋 Best Practices
116
+ ## 💡 Best Practices
76
117
 
77
118
  1. **Always use `--no-session` and `-p` (or `--mode json -p`):**
78
119
  - `--no-session`: Prevents saving dummy sessions into `~/.yorn/agent/sessions/`.
@@ -0,0 +1,155 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Cross-platform Yorn Subagent Launcher
5
+ # Compatible with Linux, macOS, Android (Termux), iOS (iSH/a-Shell), and Windows (Git Bash/WSL/MSYS2)
6
+
7
+ PROMPT=""
8
+ MODEL="${YORN_MODEL:-}"
9
+ PROVIDER="${YORN_PROVIDER:-}"
10
+ TOOLS=""
11
+ NO_TOOLS=0
12
+ SYSTEM_PROMPT=""
13
+ THINKING=""
14
+ MODE="text"
15
+ PROMPT_FILE=""
16
+
17
+ while [ $# -gt 0 ]; do
18
+ case "$1" in
19
+ -p|--prompt)
20
+ PROMPT="$2"
21
+ shift 2
22
+ ;;
23
+ -m|--model)
24
+ MODEL="$2"
25
+ shift 2
26
+ ;;
27
+ -P|--provider)
28
+ PROVIDER="$2"
29
+ shift 2
30
+ ;;
31
+ -t|--tools)
32
+ TOOLS="$2"
33
+ shift 2
34
+ ;;
35
+ -nt|--no-tools)
36
+ NO_TOOLS=1
37
+ shift
38
+ ;;
39
+ -s|--system-prompt)
40
+ SYSTEM_PROMPT="$2"
41
+ shift 2
42
+ ;;
43
+ -k|--thinking)
44
+ THINKING="$2"
45
+ shift 2
46
+ ;;
47
+ --json)
48
+ MODE="json"
49
+ shift
50
+ ;;
51
+ --mode)
52
+ MODE="$2"
53
+ shift 2
54
+ ;;
55
+ -f|--file)
56
+ PROMPT_FILE="$2"
57
+ shift 2
58
+ ;;
59
+ -h|--help)
60
+ echo "Usage: run_agent.sh [options] [prompt]"
61
+ echo ""
62
+ echo "Options:"
63
+ echo " -p, --prompt <text> Subagent task prompt"
64
+ echo " -m, --model <model> Model name (default: \$YORN_MODEL)"
65
+ echo " -P, --provider <provider> Provider name (default: \$YORN_PROVIDER)"
66
+ echo " -t, --tools <list> Comma-separated tool allowlist (e.g. read,grep,find,ls)"
67
+ echo " -nt, --no-tools Disable all tools"
68
+ echo " -s, --system-prompt <text> Custom system prompt"
69
+ echo " -k, --thinking <level> Thinking level (off, low, medium, high, max)"
70
+ echo " --json Output raw JSON event stream"
71
+ echo " -f, --file <path> Read prompt from a file"
72
+ echo " -h, --help Show this help"
73
+ exit 0
74
+ ;;
75
+ *)
76
+ if [ -z "$PROMPT" ]; then
77
+ PROMPT="$1"
78
+ else
79
+ PROMPT="$PROMPT $1"
80
+ fi
81
+ shift
82
+ ;;
83
+ esac
84
+ done
85
+
86
+ # Read prompt from file if specified
87
+ if [ -n "$PROMPT_FILE" ] && [ -f "$PROMPT_FILE" ]; then
88
+ PROMPT="$(cat "$PROMPT_FILE")"
89
+ fi
90
+
91
+ # Read prompt from stdin if not passed via args
92
+ if [ -z "$PROMPT" ] && [ ! -t 0 ]; then
93
+ PROMPT="$(cat)"
94
+ fi
95
+
96
+ if [ -z "$PROMPT" ]; then
97
+ echo "Error: No prompt provided. Specify --prompt \"...\" or pass via positional args/stdin." >&2
98
+ exit 1
99
+ fi
100
+
101
+ # Resolve yorn binary / script cross-platform
102
+ YORN_CMD=""
103
+ SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd || echo "")"
104
+
105
+ if command -v yorn >/dev/null 2>&1; then
106
+ YORN_CMD="yorn"
107
+ elif [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/../../yorn.cjs" ]; then
108
+ YORN_CMD="node \"$SCRIPT_DIR/../../yorn.cjs\""
109
+ elif [ -f "./dist/yorn.cjs" ]; then
110
+ YORN_CMD="node ./dist/yorn.cjs"
111
+ elif [ -f "../dist/yorn.cjs" ]; then
112
+ YORN_CMD="node ../dist/yorn.cjs"
113
+ elif [ -n "$APPDATA" ] && [ -f "$APPDATA/npm/node_modules/@pilllesss/yorn/dist/yorn.cjs" ]; then
114
+ YORN_CMD="node \"$APPDATA/npm/node_modules/@pilllesss/yorn/dist/yorn.cjs\""
115
+ elif [ -n "$PREFIX" ] && [ -f "$PREFIX/lib/node_modules/@pilllesss/yorn/dist/yorn.cjs" ]; then
116
+ YORN_CMD="node \"$PREFIX/lib/node_modules/@pilllesss/yorn/dist/yorn.cjs\""
117
+ elif [ -f "/data/data/com.termux/files/usr/lib/node_modules/@pilllesss/yorn/dist/yorn.cjs" ]; then
118
+ YORN_CMD="node /data/data/com.termux/files/usr/lib/node_modules/@pilllesss/yorn/dist/yorn.cjs"
119
+ else
120
+ YORN_CMD="npx --no-install @pilllesss/yorn"
121
+ fi
122
+
123
+ # Build arguments
124
+ ARGS="-p --no-session"
125
+
126
+ if [ "$MODE" = "json" ]; then
127
+ ARGS="$ARGS --mode json"
128
+ fi
129
+
130
+ if [ -n "$PROVIDER" ]; then
131
+ ARGS="$ARGS --provider $PROVIDER"
132
+ fi
133
+
134
+ if [ -n "$MODEL" ]; then
135
+ ARGS="$ARGS --model $MODEL"
136
+ fi
137
+
138
+ if [ "$NO_TOOLS" -eq 1 ]; then
139
+ ARGS="$ARGS --no-tools"
140
+ elif [ -n "$TOOLS" ]; then
141
+ ARGS="$ARGS --tools $TOOLS"
142
+ fi
143
+
144
+ if [ -n "$THINKING" ]; then
145
+ ARGS="$ARGS --thinking $THINKING"
146
+ fi
147
+
148
+ if [ -n "$SYSTEM_PROMPT" ]; then
149
+ # Escape any quotes in system prompt
150
+ ESCAPED_SYS_PROMPT="$(echo "$SYSTEM_PROMPT" | sed 's/"/\\"/g')"
151
+ ARGS="$ARGS --system-prompt \"$ESCAPED_SYS_PROMPT\""
152
+ fi
153
+
154
+ # Execute child agent
155
+ eval "$YORN_CMD $ARGS \"\$PROMPT\""