orizu 0.3.7 → 0.4.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/README.md +55 -6
- package/dist/banner.js +20 -0
- package/dist/csv.js +117 -9
- package/dist/help.js +109 -10
- package/dist/index.js +736 -44
- package/dist/setup-prompt.js +22 -0
- package/dist/skill-installer.js +243 -17
- package/dist/workspace.js +76 -0
- package/package.json +5 -1
- package/src/banner.ts +21 -0
- package/src/csv.ts +140 -10
- package/src/help.ts +109 -10
- package/src/index.ts +815 -46
- package/src/setup-prompt.ts +27 -0
- package/src/skill-installer.ts +343 -17
- package/src/workspace.ts +112 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/__init__.py +2 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +57 -3
- package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +37 -1
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +233 -17
- package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +98 -17
- package/vendor/skills/orizu-cli/SKILL.md +8 -3
- package/vendor/skills/orizu-cli/references/cli-reference.md +25 -4
- package/vendor/skills/orizu-cli/references/optimization-reports.md +2 -2
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +1 -0
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +17 -5
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface AgentSetupPromptOptions {
|
|
2
|
+
workspacePath?: string | null
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function renderAgentSetupPrompt(options?: AgentSetupPromptOptions): string {
|
|
6
|
+
const workspaceLine = options?.workspacePath
|
|
7
|
+
? `- Local Orizu artifacts belong under ${options.workspacePath} (gitignored). Keep secrets out of the repo; credentials live in ~/.config/orizu.`
|
|
8
|
+
: '- If local Orizu artifacts are needed, suggest creating a gitignored .orizu/ workspace (orizu setup can do this).'
|
|
9
|
+
|
|
10
|
+
return `Help me embed Orizu (eval-first LLM optimization) into this repository's workflow.
|
|
11
|
+
|
|
12
|
+
Start by learning the Orizu workflow:
|
|
13
|
+
1. Run \`orizu skills path --json\` and read the SKILL.md it points to, plus the references it links.
|
|
14
|
+
2. Run \`orizu --version\` and \`orizu whoami\` to confirm the CLI is installed and authenticated. If either fails, pause and tell me how to fix it (npm i -g orizu, orizu login) before continuing.
|
|
15
|
+
|
|
16
|
+
Then inspect this repository and propose how Orizu should fit it:
|
|
17
|
+
- Identify the LLM application surfaces worth evaluating (prompts, agents, pipelines) and where their inputs/outputs live.
|
|
18
|
+
- Propose Orizu team(s) and project(s) that match how this repo is organized.
|
|
19
|
+
- Propose datasets: existing data that can be uploaded now, or a concrete plan to collect examples if none exists.
|
|
20
|
+
- Propose which prompts to version in Orizu, and which scorers or LLM judges would capture quality for them.
|
|
21
|
+
- Propose runner artifacts where local execution is needed.
|
|
22
|
+
${workspaceLine}
|
|
23
|
+
|
|
24
|
+
Important boundaries:
|
|
25
|
+
- Present your plan and get my approval before creating anything on the Orizu server or changing repo files.
|
|
26
|
+
- Never duplicate Orizu runtime behavior (scoring, optimization, auth) in repo code; always go through the orizu CLI.`
|
|
27
|
+
}
|
package/src/skill-installer.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cpSync,
|
|
3
3
|
existsSync,
|
|
4
|
+
lstatSync,
|
|
4
5
|
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
5
7
|
readFileSync,
|
|
8
|
+
readlinkSync,
|
|
9
|
+
realpathSync,
|
|
6
10
|
rmSync,
|
|
11
|
+
symlinkSync,
|
|
7
12
|
writeFileSync,
|
|
8
13
|
} from 'fs'
|
|
9
|
-
import { dirname, isAbsolute, relative, resolve } from 'path'
|
|
14
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'path'
|
|
10
15
|
import { fileURLToPath } from 'url'
|
|
11
16
|
import { homedir } from 'os'
|
|
17
|
+
import { createHash } from 'crypto'
|
|
12
18
|
|
|
13
19
|
export const SKILL_NAME = 'orizu-cli'
|
|
14
20
|
export const AGENTS_START_MARKER = '<!-- orizu-cli:start -->'
|
|
@@ -16,6 +22,7 @@ export const AGENTS_END_MARKER = '<!-- orizu-cli:end -->'
|
|
|
16
22
|
|
|
17
23
|
export const SKILL_INSTALL_TARGETS = [
|
|
18
24
|
'agent-user',
|
|
25
|
+
'agents-project',
|
|
19
26
|
'codex-project',
|
|
20
27
|
'claude-user',
|
|
21
28
|
'claude-project',
|
|
@@ -24,17 +31,46 @@ export const SKILL_INSTALL_TARGETS = [
|
|
|
24
31
|
|
|
25
32
|
export type SkillInstallTarget = typeof SKILL_INSTALL_TARGETS[number]
|
|
26
33
|
|
|
34
|
+
export const SKILL_META_FILENAME = '.orizu-skill-meta.json'
|
|
35
|
+
|
|
36
|
+
export const SKILL_INSTALL_AGENTS = ['claude', 'codex'] as const
|
|
37
|
+
|
|
38
|
+
export type SkillInstallAgent = typeof SKILL_INSTALL_AGENTS[number]
|
|
39
|
+
|
|
40
|
+
export type SkillInstallScope = 'global' | 'local'
|
|
41
|
+
|
|
42
|
+
export type SkillInstallMode = 'auto' | 'link' | 'copy'
|
|
43
|
+
|
|
44
|
+
const PROJECT_LEVEL_TARGETS: readonly SkillInstallTarget[] = [
|
|
45
|
+
'agents-project',
|
|
46
|
+
'codex-project',
|
|
47
|
+
'claude-project',
|
|
48
|
+
]
|
|
49
|
+
|
|
27
50
|
export interface SkillInstallOptions {
|
|
28
51
|
cwd?: string
|
|
29
52
|
homeDir?: string
|
|
30
53
|
overwrite?: boolean
|
|
31
54
|
dryRun?: boolean
|
|
55
|
+
mode?: SkillInstallMode
|
|
56
|
+
cliVersion?: string
|
|
32
57
|
}
|
|
33
58
|
|
|
34
59
|
export interface SkillInstallResult {
|
|
35
60
|
target: SkillInstallTarget
|
|
36
61
|
path: string
|
|
37
62
|
action: 'created' | 'updated' | 'would-create' | 'would-update'
|
|
63
|
+
mode: 'link' | 'copy' | 'section'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SkillInstallMeta {
|
|
67
|
+
name: string
|
|
68
|
+
skillHash: string
|
|
69
|
+
cliVersion: string | null
|
|
70
|
+
source: SkillSourceType
|
|
71
|
+
sourceRoot: string
|
|
72
|
+
mode: 'copy'
|
|
73
|
+
installedAt: string
|
|
38
74
|
}
|
|
39
75
|
|
|
40
76
|
function getCliRoot(): string {
|
|
@@ -54,25 +90,49 @@ export function formatMissingSkillSourceError(paths: string[]): string {
|
|
|
54
90
|
+ `Searched:\n- ${paths.join('\n- ')}`
|
|
55
91
|
}
|
|
56
92
|
|
|
57
|
-
export
|
|
93
|
+
export type SkillSourceType = 'override' | 'packaged' | 'repo-fallback'
|
|
94
|
+
|
|
95
|
+
export interface SkillSourceInfo {
|
|
96
|
+
name: string
|
|
97
|
+
root: string
|
|
98
|
+
skillMd: string
|
|
99
|
+
source: SkillSourceType
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resolveSkillSource(): SkillSourceInfo {
|
|
58
103
|
const override = process.env.ORIZU_SKILL_SOURCE_DIR
|
|
59
104
|
if (override) {
|
|
60
105
|
const resolvedOverride = resolve(override)
|
|
61
106
|
if (!isSkillDir(resolvedOverride)) {
|
|
62
107
|
throw new Error(`ORIZU_SKILL_SOURCE_DIR is not an Orizu skill directory: ${resolvedOverride}`)
|
|
63
108
|
}
|
|
64
|
-
return
|
|
109
|
+
return {
|
|
110
|
+
name: SKILL_NAME,
|
|
111
|
+
root: resolvedOverride,
|
|
112
|
+
skillMd: resolve(resolvedOverride, 'SKILL.md'),
|
|
113
|
+
source: 'override',
|
|
114
|
+
}
|
|
65
115
|
}
|
|
66
116
|
|
|
67
117
|
const cliRoot = getCliRoot()
|
|
68
118
|
const packagedSkill = resolve(cliRoot, 'vendor', 'skills', SKILL_NAME)
|
|
69
119
|
if (isSkillDir(packagedSkill)) {
|
|
70
|
-
return
|
|
120
|
+
return {
|
|
121
|
+
name: SKILL_NAME,
|
|
122
|
+
root: packagedSkill,
|
|
123
|
+
skillMd: resolve(packagedSkill, 'SKILL.md'),
|
|
124
|
+
source: 'packaged',
|
|
125
|
+
}
|
|
71
126
|
}
|
|
72
127
|
|
|
73
128
|
const repoSkill = resolve(cliRoot, '..', '..', 'skills', SKILL_NAME)
|
|
74
129
|
if (isSkillDir(repoSkill)) {
|
|
75
|
-
return
|
|
130
|
+
return {
|
|
131
|
+
name: SKILL_NAME,
|
|
132
|
+
root: repoSkill,
|
|
133
|
+
skillMd: resolve(repoSkill, 'SKILL.md'),
|
|
134
|
+
source: 'repo-fallback',
|
|
135
|
+
}
|
|
76
136
|
}
|
|
77
137
|
|
|
78
138
|
const searchedPaths = [
|
|
@@ -83,6 +143,40 @@ export function resolveSkillSourceDir(): string {
|
|
|
83
143
|
throw new Error(formatMissingSkillSourceError(searchedPaths))
|
|
84
144
|
}
|
|
85
145
|
|
|
146
|
+
export function resolveSkillSourceDir(): string {
|
|
147
|
+
return resolveSkillSource().root
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function listSkillFiles(root: string, dir: string, files: string[]): void {
|
|
151
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
152
|
+
const entryPath = join(dir, entry.name)
|
|
153
|
+
if (!shouldCopySkillPath(root, entryPath)) {
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
listSkillFiles(root, entryPath, files)
|
|
158
|
+
} else if (entry.isFile()) {
|
|
159
|
+
files.push(entryPath)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function computeSkillContentHash(root: string): string {
|
|
165
|
+
const files: string[] = []
|
|
166
|
+
listSkillFiles(root, root, files)
|
|
167
|
+
const hash = createHash('sha256')
|
|
168
|
+
const sorted = files
|
|
169
|
+
.map(file => relative(root, file).replace(/\\/g, '/'))
|
|
170
|
+
.sort()
|
|
171
|
+
for (const rel of sorted) {
|
|
172
|
+
hash.update(rel)
|
|
173
|
+
hash.update('\0')
|
|
174
|
+
hash.update(readFileSync(join(root, rel)))
|
|
175
|
+
hash.update('\0')
|
|
176
|
+
}
|
|
177
|
+
return `sha256:${hash.digest('hex')}`
|
|
178
|
+
}
|
|
179
|
+
|
|
86
180
|
function resolveHome(options?: SkillInstallOptions): string {
|
|
87
181
|
return resolve(options?.homeDir || process.env.HOME || homedir())
|
|
88
182
|
}
|
|
@@ -102,6 +196,10 @@ export function getSkillInstallPath(
|
|
|
102
196
|
return resolve(home, '.agents', 'skills', SKILL_NAME)
|
|
103
197
|
}
|
|
104
198
|
|
|
199
|
+
if (target === 'agents-project') {
|
|
200
|
+
return resolve(cwd, '.agents', 'skills', SKILL_NAME)
|
|
201
|
+
}
|
|
202
|
+
|
|
105
203
|
if (target === 'codex-project') {
|
|
106
204
|
return resolve(cwd, '.codex', 'skills', SKILL_NAME)
|
|
107
205
|
}
|
|
@@ -117,6 +215,69 @@ export function getSkillInstallPath(
|
|
|
117
215
|
return resolve(cwd, 'AGENTS.md')
|
|
118
216
|
}
|
|
119
217
|
|
|
218
|
+
export function getTargetForAgent(
|
|
219
|
+
agent: SkillInstallAgent,
|
|
220
|
+
scope: SkillInstallScope
|
|
221
|
+
): SkillInstallTarget {
|
|
222
|
+
if (agent === 'claude') {
|
|
223
|
+
return scope === 'global' ? 'claude-user' : 'claude-project'
|
|
224
|
+
}
|
|
225
|
+
return scope === 'global' ? 'agent-user' : 'agents-project'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function isSkillInstallAgent(value: string): value is SkillInstallAgent {
|
|
229
|
+
return (SKILL_INSTALL_AGENTS as readonly string[]).includes(value)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function isProjectLevelTarget(target: SkillInstallTarget): boolean {
|
|
233
|
+
return PROJECT_LEVEL_TARGETS.includes(target)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const UNSTABLE_SOURCE_PATTERNS = [
|
|
237
|
+
/\/_npx\//,
|
|
238
|
+
/\/\.npm\//,
|
|
239
|
+
/\/npm-cache\//,
|
|
240
|
+
/\/\.bun\/install\/cache\//,
|
|
241
|
+
/\/bunx-/,
|
|
242
|
+
/\/dlx-/,
|
|
243
|
+
/\/\.pnpm-store\//,
|
|
244
|
+
/^\/tmp\//,
|
|
245
|
+
/^\/(private\/)?var\/folders\//,
|
|
246
|
+
/\/Temp\//,
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
export function isStableSkillSourcePath(root: string): boolean {
|
|
250
|
+
const normalized = root.replace(/\\/g, '/')
|
|
251
|
+
return !UNSTABLE_SOURCE_PATTERNS.some(pattern => pattern.test(normalized))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function resolveEffectiveInstallMode(
|
|
255
|
+
target: SkillInstallTarget,
|
|
256
|
+
sourceRoot: string,
|
|
257
|
+
mode: SkillInstallMode = 'auto'
|
|
258
|
+
): 'link' | 'copy' | 'section' {
|
|
259
|
+
if (target === 'agents-md') {
|
|
260
|
+
return 'section'
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (isProjectLevelTarget(target)) {
|
|
264
|
+
if (mode === 'link') {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`--mode link is not supported for project-level target '${target}': committed project files must not symlink to user-local package paths. Use --mode copy.`
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
return 'copy'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (mode === 'link') {
|
|
273
|
+
return 'link'
|
|
274
|
+
}
|
|
275
|
+
if (mode === 'copy') {
|
|
276
|
+
return 'copy'
|
|
277
|
+
}
|
|
278
|
+
return isStableSkillSourcePath(sourceRoot) ? 'link' : 'copy'
|
|
279
|
+
}
|
|
280
|
+
|
|
120
281
|
function shouldCopySkillPath(sourceRoot: string, sourcePath: string): boolean {
|
|
121
282
|
const rel = relative(sourceRoot, sourcePath).replace(/\\/g, '/')
|
|
122
283
|
if (!rel) {
|
|
@@ -130,7 +291,8 @@ function shouldCopySkillPath(sourceRoot: string, sourcePath: string): boolean {
|
|
|
130
291
|
return !parts.some(part =>
|
|
131
292
|
part === '.DS_Store' ||
|
|
132
293
|
part === '__pycache__' ||
|
|
133
|
-
part === '.pytest_cache'
|
|
294
|
+
part === '.pytest_cache' ||
|
|
295
|
+
part === SKILL_META_FILENAME
|
|
134
296
|
) && !rel.endsWith('.pyc') && !rel.endsWith('.pyo')
|
|
135
297
|
}
|
|
136
298
|
|
|
@@ -175,6 +337,7 @@ function installAgentsMd(
|
|
|
175
337
|
target: 'agents-md',
|
|
176
338
|
path: targetPath,
|
|
177
339
|
action: action === 'updated' ? 'would-update' : 'would-create',
|
|
340
|
+
mode: 'section',
|
|
178
341
|
}
|
|
179
342
|
}
|
|
180
343
|
|
|
@@ -185,7 +348,7 @@ function installAgentsMd(
|
|
|
185
348
|
mkdirSync(dirname(targetPath), { recursive: true })
|
|
186
349
|
if (!exists) {
|
|
187
350
|
writeFileSync(targetPath, section, 'utf8')
|
|
188
|
-
return { target: 'agents-md', path: targetPath, action: 'created' }
|
|
351
|
+
return { target: 'agents-md', path: targetPath, action: 'created', mode: 'section' }
|
|
189
352
|
}
|
|
190
353
|
|
|
191
354
|
if (hasManagedSection) {
|
|
@@ -193,53 +356,101 @@ function installAgentsMd(
|
|
|
193
356
|
`${escapeRegExp(AGENTS_START_MARKER)}[\\s\\S]*?${escapeRegExp(AGENTS_END_MARKER)}\\n?`
|
|
194
357
|
)
|
|
195
358
|
writeFileSync(targetPath, existing.replace(pattern, section), 'utf8')
|
|
196
|
-
return { target: 'agents-md', path: targetPath, action: 'updated' }
|
|
359
|
+
return { target: 'agents-md', path: targetPath, action: 'updated', mode: 'section' }
|
|
197
360
|
}
|
|
198
361
|
|
|
199
362
|
const existingPrefix = existing.trim().length === 0 ? '' : existing
|
|
200
363
|
const separator = existingPrefix.endsWith('\n') ? '\n' : '\n\n'
|
|
201
364
|
writeFileSync(targetPath, `${existingPrefix}${existingPrefix ? separator : ''}${section}`, 'utf8')
|
|
202
|
-
return { target: 'agents-md', path: targetPath, action: 'updated' }
|
|
365
|
+
return { target: 'agents-md', path: targetPath, action: 'updated', mode: 'section' }
|
|
203
366
|
}
|
|
204
367
|
|
|
205
368
|
function escapeRegExp(value: string): string {
|
|
206
369
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
207
370
|
}
|
|
208
371
|
|
|
372
|
+
function pathEntryExists(path: string): boolean {
|
|
373
|
+
try {
|
|
374
|
+
lstatSync(path)
|
|
375
|
+
return true
|
|
376
|
+
} catch {
|
|
377
|
+
return false
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
209
381
|
export function targetNeedsOverwrite(
|
|
210
382
|
target: SkillInstallTarget,
|
|
211
383
|
options?: SkillInstallOptions
|
|
212
384
|
): boolean {
|
|
213
385
|
const targetPath = getSkillInstallPath(target, options)
|
|
214
|
-
if (
|
|
215
|
-
return
|
|
386
|
+
if (target !== 'agents-md') {
|
|
387
|
+
return pathEntryExists(targetPath)
|
|
216
388
|
}
|
|
217
389
|
|
|
218
|
-
if (
|
|
219
|
-
return
|
|
390
|
+
if (!existsSync(targetPath)) {
|
|
391
|
+
return false
|
|
220
392
|
}
|
|
221
393
|
|
|
222
394
|
const existing = readFileSync(targetPath, 'utf8')
|
|
223
395
|
return existing.includes(AGENTS_START_MARKER) && existing.includes(AGENTS_END_MARKER)
|
|
224
396
|
}
|
|
225
397
|
|
|
398
|
+
function writeSkillMeta(
|
|
399
|
+
targetDir: string,
|
|
400
|
+
source: SkillSourceInfo,
|
|
401
|
+
options?: SkillInstallOptions
|
|
402
|
+
): void {
|
|
403
|
+
const meta: SkillInstallMeta = {
|
|
404
|
+
name: source.name,
|
|
405
|
+
skillHash: computeSkillContentHash(source.root),
|
|
406
|
+
cliVersion: options?.cliVersion || null,
|
|
407
|
+
source: source.source,
|
|
408
|
+
sourceRoot: source.root,
|
|
409
|
+
mode: 'copy',
|
|
410
|
+
installedAt: new Date().toISOString(),
|
|
411
|
+
}
|
|
412
|
+
writeFileSync(join(targetDir, SKILL_META_FILENAME), `${JSON.stringify(meta, null, 2)}\n`, 'utf8')
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function readSkillMeta(targetDir: string): SkillInstallMeta | null {
|
|
416
|
+
const metaPath = join(targetDir, SKILL_META_FILENAME)
|
|
417
|
+
if (!existsSync(metaPath)) {
|
|
418
|
+
return null
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(readFileSync(metaPath, 'utf8')) as SkillInstallMeta
|
|
422
|
+
return parsed && typeof parsed === 'object' ? parsed : null
|
|
423
|
+
} catch {
|
|
424
|
+
return null
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function linkSkillTree(sourceDir: string, targetDir: string) {
|
|
429
|
+
rmSync(targetDir, { recursive: true, force: true })
|
|
430
|
+
mkdirSync(dirname(targetDir), { recursive: true })
|
|
431
|
+
symlinkSync(sourceDir, targetDir, 'dir')
|
|
432
|
+
}
|
|
433
|
+
|
|
226
434
|
export function installSkillTarget(
|
|
227
435
|
target: SkillInstallTarget,
|
|
228
436
|
options?: SkillInstallOptions
|
|
229
437
|
): SkillInstallResult {
|
|
230
|
-
const
|
|
438
|
+
const source = resolveSkillSource()
|
|
231
439
|
const targetPath = getSkillInstallPath(target, options)
|
|
232
|
-
const exists = existsSync(targetPath)
|
|
233
440
|
|
|
234
441
|
if (target === 'agents-md') {
|
|
235
|
-
return installAgentsMd(
|
|
442
|
+
return installAgentsMd(source.root, targetPath, options)
|
|
236
443
|
}
|
|
237
444
|
|
|
445
|
+
const exists = pathEntryExists(targetPath)
|
|
446
|
+
const mode = resolveEffectiveInstallMode(target, source.root, options?.mode)
|
|
447
|
+
|
|
238
448
|
if (options?.dryRun) {
|
|
239
449
|
return {
|
|
240
450
|
target,
|
|
241
451
|
path: targetPath,
|
|
242
452
|
action: exists ? 'would-update' : 'would-create',
|
|
453
|
+
mode,
|
|
243
454
|
}
|
|
244
455
|
}
|
|
245
456
|
|
|
@@ -247,11 +458,126 @@ export function installSkillTarget(
|
|
|
247
458
|
throw new Error(`${targetPath} already exists. Pass --yes to replace it.`)
|
|
248
459
|
}
|
|
249
460
|
|
|
250
|
-
|
|
461
|
+
if (mode === 'link') {
|
|
462
|
+
linkSkillTree(source.root, targetPath)
|
|
463
|
+
} else {
|
|
464
|
+
copySkillTree(source.root, targetPath)
|
|
465
|
+
writeSkillMeta(targetPath, source, options)
|
|
466
|
+
}
|
|
467
|
+
|
|
251
468
|
return {
|
|
252
469
|
target,
|
|
253
470
|
path: targetPath,
|
|
254
471
|
action: exists ? 'updated' : 'created',
|
|
472
|
+
mode,
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export type SkillTargetState =
|
|
477
|
+
| 'current'
|
|
478
|
+
| 'stale'
|
|
479
|
+
| 'missing'
|
|
480
|
+
| 'broken-link'
|
|
481
|
+
| 'unmanaged'
|
|
482
|
+
|
|
483
|
+
export interface SkillTargetStatus {
|
|
484
|
+
target: SkillInstallTarget
|
|
485
|
+
path: string
|
|
486
|
+
state: SkillTargetState
|
|
487
|
+
mode: 'link' | 'copy' | 'section' | null
|
|
488
|
+
linkTarget: string | null
|
|
489
|
+
installedHash: string | null
|
|
490
|
+
sourceHash: string
|
|
491
|
+
meta: SkillInstallMeta | null
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function getAgentsMdStatus(
|
|
495
|
+
targetPath: string,
|
|
496
|
+
source: SkillSourceInfo,
|
|
497
|
+
sourceHash: string
|
|
498
|
+
): SkillTargetStatus {
|
|
499
|
+
const base = {
|
|
500
|
+
target: 'agents-md' as const,
|
|
501
|
+
path: targetPath,
|
|
502
|
+
mode: 'section' as const,
|
|
503
|
+
linkTarget: null,
|
|
504
|
+
installedHash: null,
|
|
505
|
+
sourceHash,
|
|
506
|
+
meta: null,
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!existsSync(targetPath)) {
|
|
510
|
+
return { ...base, state: 'missing' }
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const existing = readFileSync(targetPath, 'utf8')
|
|
514
|
+
const start = existing.indexOf(AGENTS_START_MARKER)
|
|
515
|
+
const end = existing.indexOf(AGENTS_END_MARKER)
|
|
516
|
+
if (start === -1 || end === -1) {
|
|
517
|
+
return { ...base, state: 'unmanaged' }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const actualSection = existing.slice(start, end + AGENTS_END_MARKER.length)
|
|
521
|
+
const expectedSection = buildAgentsSection(source.root).trimEnd()
|
|
522
|
+
return {
|
|
523
|
+
...base,
|
|
524
|
+
state: actualSection.trimEnd() === expectedSection ? 'current' : 'stale',
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function getSkillTargetStatus(
|
|
529
|
+
target: SkillInstallTarget,
|
|
530
|
+
options?: SkillInstallOptions
|
|
531
|
+
): SkillTargetStatus {
|
|
532
|
+
const source = resolveSkillSource()
|
|
533
|
+
const sourceHash = computeSkillContentHash(source.root)
|
|
534
|
+
const targetPath = getSkillInstallPath(target, options)
|
|
535
|
+
|
|
536
|
+
if (target === 'agents-md') {
|
|
537
|
+
return getAgentsMdStatus(targetPath, source, sourceHash)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const base = {
|
|
541
|
+
target,
|
|
542
|
+
path: targetPath,
|
|
543
|
+
linkTarget: null as string | null,
|
|
544
|
+
installedHash: null as string | null,
|
|
545
|
+
sourceHash,
|
|
546
|
+
meta: null as SkillInstallMeta | null,
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!pathEntryExists(targetPath)) {
|
|
550
|
+
return { ...base, state: 'missing', mode: null }
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const stats = lstatSync(targetPath)
|
|
554
|
+
if (stats.isSymbolicLink()) {
|
|
555
|
+
const linkTarget = readlinkSync(targetPath)
|
|
556
|
+
if (!existsSync(targetPath)) {
|
|
557
|
+
return { ...base, state: 'broken-link', mode: 'link', linkTarget }
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const resolvedLink = realpathSync(targetPath)
|
|
561
|
+
const installedHash = computeSkillContentHash(resolvedLink)
|
|
562
|
+
const state = resolvedLink === realpathSync(source.root) || installedHash === sourceHash
|
|
563
|
+
? 'current'
|
|
564
|
+
: 'stale'
|
|
565
|
+
return { ...base, state, mode: 'link', linkTarget, installedHash }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const installedHash = computeSkillContentHash(targetPath)
|
|
569
|
+
const meta = readSkillMeta(targetPath)
|
|
570
|
+
// A differing directory without our meta file was not installed by Orizu
|
|
571
|
+
// (or was hand-customized): treat it as unmanaged so `skills update` never
|
|
572
|
+
// silently overwrites it. Matching content is current regardless of meta.
|
|
573
|
+
const state =
|
|
574
|
+
installedHash === sourceHash ? 'current' : meta ? 'stale' : 'unmanaged'
|
|
575
|
+
return {
|
|
576
|
+
...base,
|
|
577
|
+
state,
|
|
578
|
+
mode: 'copy',
|
|
579
|
+
installedHash,
|
|
580
|
+
meta,
|
|
255
581
|
}
|
|
256
582
|
}
|
|
257
583
|
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
+
import { join, resolve } from 'path'
|
|
3
|
+
|
|
4
|
+
export const WORKSPACE_DIR_NAME = '.orizu'
|
|
5
|
+
|
|
6
|
+
export interface WorkspaceInitOptions {
|
|
7
|
+
cwd?: string
|
|
8
|
+
baseUrl?: string | null
|
|
9
|
+
cliVersion?: string | null
|
|
10
|
+
dryRun?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WorkspaceInitResult {
|
|
14
|
+
root: string
|
|
15
|
+
state: 'created' | 'exists' | 'would-create'
|
|
16
|
+
gitignoreUpdated: boolean
|
|
17
|
+
actions: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const WORKSPACE_README = `# Orizu workspace
|
|
21
|
+
|
|
22
|
+
Local Orizu artifacts for this repository. This folder is gitignored by
|
|
23
|
+
default: generated and server-derived content here is not guaranteed to stay
|
|
24
|
+
in sync with the Orizu server, so checking it in would risk drift. Safe,
|
|
25
|
+
user-authored configuration may move to a committed location once a sync model
|
|
26
|
+
exists.
|
|
27
|
+
|
|
28
|
+
- \`workspace.json\`: safe local metadata (no secrets; credentials stay in
|
|
29
|
+
\`~/.config/orizu\`).
|
|
30
|
+
- \`generated/\`: runner artifacts, exports, and other generated files.
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
export function getWorkspaceRoot(cwd?: string): string {
|
|
34
|
+
return resolve(cwd || process.cwd(), WORKSPACE_DIR_NAME)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function workspaceExists(cwd?: string): boolean {
|
|
38
|
+
return existsSync(join(getWorkspaceRoot(cwd), 'workspace.json'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureGitignoreEntry(cwd: string, dryRun: boolean): { updated: boolean, action: string | null } {
|
|
42
|
+
const gitignorePath = resolve(cwd, '.gitignore')
|
|
43
|
+
const entry = `${WORKSPACE_DIR_NAME}/`
|
|
44
|
+
|
|
45
|
+
if (existsSync(gitignorePath)) {
|
|
46
|
+
const existing = readFileSync(gitignorePath, 'utf8')
|
|
47
|
+
const hasEntry = existing
|
|
48
|
+
.split('\n')
|
|
49
|
+
.some(line => line.trim() === entry || line.trim() === WORKSPACE_DIR_NAME)
|
|
50
|
+
if (hasEntry) {
|
|
51
|
+
return { updated: false, action: null }
|
|
52
|
+
}
|
|
53
|
+
if (!dryRun) {
|
|
54
|
+
const separator = existing.endsWith('\n') || existing.length === 0 ? '' : '\n'
|
|
55
|
+
writeFileSync(gitignorePath, `${existing}${separator}\n# Local Orizu workspace (generated; not synced with the server)\n${entry}\n`, 'utf8')
|
|
56
|
+
}
|
|
57
|
+
return { updated: true, action: `append ${entry} to .gitignore` }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!dryRun) {
|
|
61
|
+
writeFileSync(gitignorePath, `# Local Orizu workspace (generated; not synced with the server)\n${entry}\n`, 'utf8')
|
|
62
|
+
}
|
|
63
|
+
return { updated: true, action: `create .gitignore with ${entry}` }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function initOrizuWorkspace(options?: WorkspaceInitOptions): WorkspaceInitResult {
|
|
67
|
+
const cwd = resolve(options?.cwd || process.cwd())
|
|
68
|
+
const root = getWorkspaceRoot(cwd)
|
|
69
|
+
const dryRun = Boolean(options?.dryRun)
|
|
70
|
+
const exists = workspaceExists(cwd)
|
|
71
|
+
const actions: string[] = []
|
|
72
|
+
|
|
73
|
+
if (!exists) {
|
|
74
|
+
actions.push(`create ${WORKSPACE_DIR_NAME}/workspace.json, README.md, and generated/`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const gitignore = ensureGitignoreEntry(cwd, dryRun || exists)
|
|
78
|
+
if (!exists && gitignore.action) {
|
|
79
|
+
actions.push(gitignore.action)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (dryRun) {
|
|
83
|
+
return {
|
|
84
|
+
root,
|
|
85
|
+
state: exists ? 'exists' : 'would-create',
|
|
86
|
+
gitignoreUpdated: false,
|
|
87
|
+
actions,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (exists) {
|
|
92
|
+
return { root, state: 'exists', gitignoreUpdated: false, actions: [] }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
mkdirSync(join(root, 'generated'), { recursive: true })
|
|
96
|
+
writeFileSync(join(root, 'README.md'), WORKSPACE_README, 'utf8')
|
|
97
|
+
writeFileSync(
|
|
98
|
+
join(root, 'workspace.json'),
|
|
99
|
+
`${JSON.stringify(
|
|
100
|
+
{
|
|
101
|
+
version: 1,
|
|
102
|
+
server: options?.baseUrl || null,
|
|
103
|
+
createdByCliVersion: options?.cliVersion || null,
|
|
104
|
+
},
|
|
105
|
+
null,
|
|
106
|
+
2
|
|
107
|
+
)}\n`,
|
|
108
|
+
'utf8'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return { root, state: 'created', gitignoreUpdated: gitignore.updated, actions }
|
|
112
|
+
}
|
|
@@ -3,6 +3,7 @@ from .optimizer import (
|
|
|
3
3
|
DatasetRow,
|
|
4
4
|
NumThreadsPlan,
|
|
5
5
|
PromptContext,
|
|
6
|
+
RetryableReflectionError,
|
|
6
7
|
RowEvaluation,
|
|
7
8
|
TextGepaConfig,
|
|
8
9
|
TextGepaResult,
|
|
@@ -15,6 +16,7 @@ __all__ = [
|
|
|
15
16
|
"DatasetRow",
|
|
16
17
|
"NumThreadsPlan",
|
|
17
18
|
"PromptContext",
|
|
19
|
+
"RetryableReflectionError",
|
|
18
20
|
"RowEvaluation",
|
|
19
21
|
"TextGepaConfig",
|
|
20
22
|
"TextGepaResult",
|