orizu 0.4.1 → 0.5.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 (76) hide show
  1. package/README.md +22 -13
  2. package/dist/artifact-archive.js +152 -0
  3. package/dist/connectors-cli.js +80 -0
  4. package/dist/credentials.js +4 -1
  5. package/dist/daytona-slice-rehearsal.js +255 -0
  6. package/dist/daytona-slice.js +441 -0
  7. package/dist/egress-allowlist-cli.js +132 -0
  8. package/dist/egress-policy.js +159 -0
  9. package/dist/git-credential.js +131 -0
  10. package/dist/github-setup.js +226 -0
  11. package/dist/help.js +271 -16
  12. package/dist/hosted-bootstrap-rehearsal.js +235 -0
  13. package/dist/hosted-bootstrap.js +365 -0
  14. package/dist/hosted-broker-server.js +220 -0
  15. package/dist/hosted-git-http-server.js +136 -0
  16. package/dist/hosted-harness-claude.js +493 -0
  17. package/dist/hosted-harness-opencode.js +729 -0
  18. package/dist/hosted-harness.js +60 -0
  19. package/dist/hosted-loop.js +477 -0
  20. package/dist/hosted-run-event-sink.js +499 -0
  21. package/dist/hosted-runtime-assets.js +216 -0
  22. package/dist/hosted-session-cli.js +642 -0
  23. package/dist/index.js +855 -237
  24. package/dist/kill-switch-cli.js +89 -0
  25. package/dist/manifests-cli.js +143 -0
  26. package/dist/preview-runtime.js +4 -1
  27. package/dist/sandbox-provider.js +238 -0
  28. package/dist/secret-redaction.js +112 -0
  29. package/dist/session-timeline.js +103 -0
  30. package/dist/setup-prompt.js +4 -2
  31. package/dist/skill-installer.js +9 -1
  32. package/dist/smoke.js +250 -0
  33. package/dist/task-assignment-manifest.js +39 -2
  34. package/dist/vercel-sandbox-provider.js +260 -0
  35. package/dist/workbench-cli.js +493 -0
  36. package/dist/workspace-sync.js +924 -0
  37. package/dist/workspace.js +868 -57
  38. package/package.json +4 -1
  39. package/src/artifact-archive.ts +182 -0
  40. package/src/connectors-cli.ts +115 -0
  41. package/src/credentials.ts +6 -2
  42. package/src/daytona-slice-rehearsal.ts +320 -0
  43. package/src/daytona-slice.ts +598 -0
  44. package/src/egress-allowlist-cli.ts +147 -0
  45. package/src/egress-policy.ts +188 -0
  46. package/src/git-credential.ts +155 -0
  47. package/src/github-setup.ts +366 -0
  48. package/src/help.ts +272 -16
  49. package/src/hosted-bootstrap-rehearsal.ts +312 -0
  50. package/src/hosted-bootstrap.ts +571 -0
  51. package/src/hosted-broker-server.ts +278 -0
  52. package/src/hosted-git-http-server.ts +164 -0
  53. package/src/hosted-harness-claude.ts +621 -0
  54. package/src/hosted-harness-opencode.ts +823 -0
  55. package/src/hosted-harness.ts +124 -0
  56. package/src/hosted-loop.ts +680 -0
  57. package/src/hosted-run-event-sink.ts +632 -0
  58. package/src/hosted-runtime-assets.ts +272 -0
  59. package/src/hosted-session-cli.ts +844 -0
  60. package/src/index.ts +987 -296
  61. package/src/kill-switch-cli.ts +113 -0
  62. package/src/manifests-cli.ts +168 -0
  63. package/src/preview-runtime.ts +14 -9
  64. package/src/sandbox-provider.ts +440 -0
  65. package/src/secret-redaction.ts +124 -0
  66. package/src/session-timeline.ts +152 -0
  67. package/src/setup-prompt.ts +4 -2
  68. package/src/skill-installer.ts +13 -1
  69. package/src/smoke.ts +342 -0
  70. package/src/task-assignment-manifest.ts +70 -2
  71. package/src/vercel-sandbox-provider.ts +388 -0
  72. package/src/workbench-cli.ts +701 -0
  73. package/src/workspace-sync.ts +1148 -0
  74. package/src/workspace.ts +1051 -64
  75. package/vendor/skills/orizu-cli/SKILL.md +3 -1
  76. package/vendor/skills/orizu-cli/references/cli-reference.md +3 -3
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash, randomBytes, randomUUID } from 'crypto'
3
- import { basename, delimiter, dirname, extname, isAbsolute, join, normalize } from 'path'
3
+ import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve } from 'path'
4
4
  import { fileURLToPath } from 'url'
5
5
  import { createServer } from 'http'
6
6
  import { gunzipSync, gzipSync } from 'zlib'
@@ -8,7 +8,6 @@ import {
8
8
  existsSync,
9
9
  mkdirSync,
10
10
  mkdtempSync,
11
- readdirSync,
12
11
  readFileSync,
13
12
  realpathSync,
14
13
  rmSync,
@@ -16,8 +15,9 @@ import {
16
15
  writeFileSync,
17
16
  } from 'fs'
18
17
  import { spawn, spawnSync } from 'child_process'
19
- import { tmpdir } from 'os'
18
+ import { homedir, tmpdir } from 'os'
20
19
  import { createInterface } from 'readline/promises'
20
+ import { emitKeypressEvents } from 'readline'
21
21
  import { stdin as input, stdout as output } from 'process'
22
22
  import { clearServerCredentials, getServerCredentials, saveServerCredentials } from './credentials.js'
23
23
  import { parseDatasetFile } from './file-parser.js'
@@ -26,15 +26,10 @@ import { parseDatasetReference } from './dataset-download.js'
26
26
  import { extractErrorMessage } from './error-response.js'
27
27
  import { parseGlobalFlags } from './global-flags.js'
28
28
  import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js'
29
+ import { zipDirectoryToBase64 } from './artifact-archive.js'
29
30
  import { runLocalAppPreview } from './preview-runtime.js'
30
- import { parseAssignmentManifestJsonl } from './task-assignment-manifest.js'
31
- import {
32
- assertSecureTokenTransport,
33
- authedFetch,
34
- getBaseUrl,
35
- resolveLoginBaseUrl,
36
- setGlobalFlags,
37
- } from './http.js'
31
+ import { readAssignmentManifestJsonlFile } from './task-assignment-manifest.js'
32
+ import { assertSecureTokenTransport, authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags } from './http.js'
38
33
  import {
39
34
  computeSkillContentHash,
40
35
  getSkillInstallPath,
@@ -46,6 +41,7 @@ import {
46
41
  resolveSkillSource,
47
42
  SKILL_INSTALL_AGENTS,
48
43
  SKILL_INSTALL_TARGETS,
44
+ SkillInstallAgent,
49
45
  SkillInstallMode,
50
46
  SkillInstallScope,
51
47
  SkillInstallTarget,
@@ -59,9 +55,26 @@ import { LoginResponse } from './types.js'
59
55
  import {
60
56
  getWorkspaceRoot,
61
57
  initOrizuWorkspace,
62
- WORKSPACE_DIR_NAME,
58
+ type WorkspaceProjectSeed,
63
59
  workspaceExists,
64
60
  } from './workspace.js'
61
+ import { connectorsCommand } from './connectors-cli.js'
62
+ import { killSwitchCommand } from './kill-switch-cli.js'
63
+ import { egressAllowlistCommand } from './egress-allowlist-cli.js'
64
+ import { manifestsCommand } from './manifests-cli.js'
65
+ import { workbenchCommand } from './workbench-cli.js'
66
+ import { hostedCommand } from './hosted-session-cli.js'
67
+ import { workspaceSyncCommand } from './workspace-sync.js'
68
+ import { runGitCredential } from './git-credential.js'
69
+ import { type GithubLinkResult, runGithubLink, runInteractiveHostedSetup } from './github-setup.js'
70
+
71
+ function getErrorMessage(error: unknown): string {
72
+ return error instanceof Error ? error.message : String(error)
73
+ }
74
+
75
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
76
+ return error instanceof Error
77
+ }
65
78
 
66
79
  interface Team {
67
80
  id: string
@@ -279,6 +292,21 @@ interface TaskStatusPayload {
279
292
  }
280
293
  }
281
294
 
295
+ const TASK_STATUS_ACTION_LABELS = {
296
+ paused: {
297
+ verb: 'pause',
298
+ pastTense: 'Paused',
299
+ },
300
+ active: {
301
+ verb: 'unpause',
302
+ pastTense: 'Unpaused',
303
+ },
304
+ completed: {
305
+ verb: 'complete',
306
+ pastTense: 'Completed',
307
+ },
308
+ } as const
309
+
282
310
  function getCliVersion(): string {
283
311
  const packageJson = JSON.parse(
284
312
  readFileSync(new URL('../package.json', import.meta.url), 'utf8')
@@ -312,14 +340,6 @@ function printUsage() {
312
340
  printLine(renderRootHelp())
313
341
  }
314
342
 
315
- function printPreviewUsage() {
316
- printLine(`\nLocal app preview:\n\n orizu apps preview --file <path> --input-schema <json-path> --output-schema <json-path> --sample-row <json-path> [--screenshot <png-path>] [--headed] [--keep-open] [--component <name>]`)
317
- }
318
-
319
- function printOptimizationUsage() {
320
- printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs] [--no-skip-perfect-parent-reflection]\n orizu optimizations export <run-id> [--out <path>] [--json]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`)
321
- }
322
-
323
343
  let cliArgs = process.argv.slice(2)
324
344
  let cliJsonOutput = false
325
345
 
@@ -351,6 +371,20 @@ function getArgs(name: string): string[] {
351
371
  return values
352
372
  }
353
373
 
374
+ function getOptionalArgValue(name: string): string | null {
375
+ const index = cliArgs.indexOf(name)
376
+ if (index === -1 || index + 1 >= cliArgs.length) {
377
+ return null
378
+ }
379
+
380
+ const value = cliArgs[index + 1]
381
+ if (!value || value.startsWith('-')) {
382
+ return null
383
+ }
384
+
385
+ return value
386
+ }
387
+
354
388
  function rejectDashPrefixedOptionValue(name: string, value: string | null) {
355
389
  if (value && value.startsWith('-')) {
356
390
  throw new Error(`Invalid value for ${name}: option values cannot start with a dash`)
@@ -409,6 +443,10 @@ export function sanitizeTerminalText(value: unknown): string {
409
443
  return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '')
410
444
  }
411
445
 
446
+ function printError(message: string): void {
447
+ console.error(sanitizeTerminalText(message))
448
+ }
449
+
412
450
  export function validateBrowserUrl(url: string, expectedOrigin?: string): URL {
413
451
  let parsed: URL
414
452
  try {
@@ -576,8 +614,8 @@ function readJsonObjectArg(valueArg: string | null, label: string): Record<strin
576
614
  throw new Error('JSON root must be an object')
577
615
  }
578
616
  return parsed as Record<string, unknown>
579
- } catch (error: any) {
580
- throw new Error(`Invalid ${label} '${valueArg}': ${error?.message || String(error)}`)
617
+ } catch (error: unknown) {
618
+ throw new Error(`Invalid ${label} '${valueArg}': ${getErrorMessage(error)}`)
581
619
  }
582
620
  }
583
621
 
@@ -689,6 +727,55 @@ async function fetchTeamMembers(teamSlug: string): Promise<TeamMember[]> {
689
727
  return data.members
690
728
  }
691
729
 
730
+ async function createTeamOnServer(name: string): Promise<Team> {
731
+ const response = await authedFetch('/api/cli/teams', {
732
+ method: 'POST',
733
+ headers: { 'Content-Type': 'application/json' },
734
+ body: JSON.stringify({ name }),
735
+ })
736
+
737
+ if (!response.ok) {
738
+ throw new Error(`Failed to create team: ${await response.text()}`)
739
+ }
740
+
741
+ const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
742
+ return data.team
743
+ }
744
+
745
+ async function createProjectOnServer(teamSlug: string, name: string): Promise<Project> {
746
+ const response = await authedFetch('/api/cli/projects', {
747
+ method: 'POST',
748
+ headers: { 'Content-Type': 'application/json' },
749
+ body: JSON.stringify({ teamSlug, name }),
750
+ })
751
+
752
+ if (!response.ok) {
753
+ throw new Error(`Failed to create project: ${await response.text()}`)
754
+ }
755
+
756
+ const data = await parseJsonResponse<{
757
+ project: {
758
+ id: string
759
+ name: string
760
+ slug: string
761
+ teamId?: string
762
+ teamName?: string
763
+ teamSlug: string
764
+ role?: string
765
+ }
766
+ }>(response, 'Project create')
767
+
768
+ return {
769
+ id: data.project.id,
770
+ name: data.project.name,
771
+ slug: data.project.slug,
772
+ teamId: data.project.teamId || '',
773
+ teamName: data.project.teamName || data.project.teamSlug,
774
+ teamSlug: data.project.teamSlug,
775
+ role: data.project.role || 'admin',
776
+ }
777
+ }
778
+
692
779
  async function resolveProjectSelection(projectArg: string | null): Promise<Project> {
693
780
  projectArg = projectArg || process.env.ORIZU_PROJECT || null
694
781
  const teams = await fetchTeams()
@@ -724,7 +811,7 @@ async function resolveProjectSelection(projectArg: string | null): Promise<Proje
724
811
 
725
812
  const matchedTeam = teams.find(team => team.slug === teamSlug)
726
813
  if (!matchedTeam) {
727
- console.error(`Team '${sanitizeTerminalText(teamSlug)}' not found in your accessible teams.`)
814
+ printError(`Team '${teamSlug}' not found in your accessible teams.`)
728
815
  const selectedTeam = await promptSelect(
729
816
  'Select a team',
730
817
  teams,
@@ -745,7 +832,7 @@ async function resolveProjectSelection(projectArg: string | null): Promise<Proje
745
832
  const matchedProject = projects.find(project => project.slug === projectSlug)
746
833
 
747
834
  if (!matchedProject) {
748
- console.error(`Project '${sanitizeTerminalText(projectSlug)}' not found in team '${sanitizeTerminalText(matchedTeam.slug)}'.`)
835
+ printError(`Project '${projectSlug}' not found in team '${matchedTeam.slug}'.`)
749
836
  const selectedProject = await promptSelect(
750
837
  `Select a project in ${matchedTeam.slug}`,
751
838
  projects,
@@ -2771,22 +2858,12 @@ async function createTeam() {
2771
2858
  throw new Error('Usage: orizu teams create --name <name>')
2772
2859
  }
2773
2860
 
2774
- const response = await authedFetch('/api/cli/teams', {
2775
- method: 'POST',
2776
- headers: { 'Content-Type': 'application/json' },
2777
- body: JSON.stringify({ name }),
2778
- })
2779
-
2780
- if (!response.ok) {
2781
- throw new Error(`Failed to create team: ${await response.text()}`)
2782
- }
2783
-
2784
- const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
2861
+ const team = await createTeamOnServer(name)
2785
2862
  if (hasJsonFlag()) {
2786
- printJson({ team: data.team })
2863
+ printJson({ team })
2787
2864
  return
2788
2865
  }
2789
- printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`)
2866
+ printLine(`Created team: ${sanitizeTerminalText(team.name)} (${sanitizeTerminalText(team.slug)})`)
2790
2867
  }
2791
2868
 
2792
2869
  async function listProjects() {
@@ -2819,24 +2896,12 @@ async function createProject() {
2819
2896
  teamSlug = team.slug
2820
2897
  }
2821
2898
 
2822
- const response = await authedFetch('/api/cli/projects', {
2823
- method: 'POST',
2824
- headers: { 'Content-Type': 'application/json' },
2825
- body: JSON.stringify({ teamSlug, name }),
2826
- })
2827
-
2828
- if (!response.ok) {
2829
- throw new Error(`Failed to create project: ${await response.text()}`)
2830
- }
2831
-
2832
- const data = await parseJsonResponse<{
2833
- project: { id: string; name: string; slug: string; teamSlug: string }
2834
- }>(response, 'Project create')
2899
+ const project = await createProjectOnServer(teamSlug, name)
2835
2900
  if (hasJsonFlag()) {
2836
- printJson({ project: data.project })
2901
+ printJson({ project })
2837
2902
  return
2838
2903
  }
2839
- printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`)
2904
+ printLine(`Created project ${sanitizeTerminalText(`${project.teamSlug}/${project.slug}`)}`)
2840
2905
  }
2841
2906
 
2842
2907
  async function listTasks() {
@@ -2863,11 +2928,11 @@ function readSourceFile(pathArg: string): string {
2863
2928
  const expandedPath = expandHomePath(pathArg)
2864
2929
  try {
2865
2930
  return readFileSync(expandedPath, 'utf-8')
2866
- } catch (error: any) {
2867
- if (error?.code === 'ENOENT') {
2931
+ } catch (error: unknown) {
2932
+ if (isNodeError(error) && error.code === 'ENOENT') {
2868
2933
  throw new Error(`File not found: ${expandedPath}`)
2869
2934
  }
2870
- throw new Error(`Failed to read file '${expandedPath}': ${error?.message || String(error)}`)
2935
+ throw new Error(`Failed to read file '${expandedPath}': ${getErrorMessage(error)}`)
2871
2936
  }
2872
2937
  }
2873
2938
 
@@ -2875,11 +2940,11 @@ function readSourceBytes(pathArg: string): Buffer {
2875
2940
  const expandedPath = expandHomePath(pathArg)
2876
2941
  try {
2877
2942
  return readFileSync(expandedPath)
2878
- } catch (error: any) {
2879
- if (error?.code === 'ENOENT') {
2943
+ } catch (error: unknown) {
2944
+ if (isNodeError(error) && error.code === 'ENOENT') {
2880
2945
  throw new Error(`File not found: ${expandedPath}`)
2881
2946
  }
2882
- throw new Error(`Failed to read file '${expandedPath}': ${error?.message || String(error)}`)
2947
+ throw new Error(`Failed to read file '${expandedPath}': ${getErrorMessage(error)}`)
2883
2948
  }
2884
2949
  }
2885
2950
 
@@ -2921,6 +2986,304 @@ async function askYesNo(question: string, defaultYes: boolean): Promise<boolean>
2921
2986
  }
2922
2987
  }
2923
2988
 
2989
+ async function askText(question: string, defaultValue: string): Promise<string> {
2990
+ const rl = createInterface({ input, output })
2991
+ try {
2992
+ const suffix = defaultValue ? ` [${defaultValue}]` : ''
2993
+ const answer = (await rl.question(`${question}${suffix} `)).trim()
2994
+ return answer || defaultValue
2995
+ } finally {
2996
+ rl.close()
2997
+ }
2998
+ }
2999
+
3000
+ type KeypressInfo = {
3001
+ name?: string
3002
+ sequence?: string
3003
+ ctrl?: boolean
3004
+ }
3005
+
3006
+ type RawInput = typeof input & {
3007
+ setRawMode?: (mode: boolean) => void
3008
+ isRaw?: boolean
3009
+ }
3010
+
3011
+ async function waitForAnyKeyOrEscape(message: string): Promise<boolean> {
3012
+ if (!isInteractiveTerminal()) {
3013
+ throw new Error('Interactive terminal required.')
3014
+ }
3015
+
3016
+ printLine(message)
3017
+ const rawInput = input as RawInput
3018
+ const wasRaw = Boolean(rawInput.isRaw)
3019
+ emitKeypressEvents(input)
3020
+
3021
+ return new Promise((resolvePromise, rejectPromise) => {
3022
+ function cleanup() {
3023
+ input.off('keypress', onKeypress)
3024
+ if (rawInput.setRawMode && !wasRaw) {
3025
+ rawInput.setRawMode(false)
3026
+ }
3027
+ input.pause()
3028
+ printLine('')
3029
+ }
3030
+
3031
+ function onKeypress(_chunk: string, key: KeypressInfo) {
3032
+ cleanup()
3033
+ if (key.ctrl && key.name === 'c') {
3034
+ rejectPromise(new Error('Setup cancelled.'))
3035
+ return
3036
+ }
3037
+ resolvePromise(key.name !== 'escape')
3038
+ }
3039
+
3040
+ input.on('keypress', onKeypress)
3041
+ input.resume()
3042
+ rawInput.setRawMode?.(true)
3043
+ })
3044
+ }
3045
+
3046
+ async function promptKeyboardSelect<T>(
3047
+ title: string,
3048
+ items: T[],
3049
+ label: (item: T, index: number) => string
3050
+ ): Promise<T> {
3051
+ if (items.length === 0) {
3052
+ throw new Error(`No options available for ${title.toLowerCase()}`)
3053
+ }
3054
+ if (!isInteractiveTerminal()) {
3055
+ throw new Error(`${title} selection requires interactive terminal. Provide flags explicitly instead.`)
3056
+ }
3057
+
3058
+ printLine(`\n${sanitizeTerminalText(title)}`)
3059
+ printLine('Use ↑/↓ to choose, Enter to confirm, or Esc to cancel.')
3060
+
3061
+ let selected = 0
3062
+ let rendered = false
3063
+ const rawInput = input as RawInput
3064
+ const wasRaw = Boolean(rawInput.isRaw)
3065
+ emitKeypressEvents(input)
3066
+
3067
+ function render() {
3068
+ if (rendered) {
3069
+ output.write(`\x1b[${items.length}A`)
3070
+ }
3071
+ items.forEach((item, index) => {
3072
+ output.write('\x1b[2K')
3073
+ const marker = index === selected ? '●' : '○'
3074
+ output.write(` ${marker} ${sanitizeTerminalText(label(item, index))}\n`)
3075
+ })
3076
+ rendered = true
3077
+ }
3078
+
3079
+ render()
3080
+ output.write('\x1b[?25l')
3081
+
3082
+ return new Promise((resolvePromise, rejectPromise) => {
3083
+ function cleanup() {
3084
+ input.off('keypress', onKeypress)
3085
+ if (rawInput.setRawMode && !wasRaw) {
3086
+ rawInput.setRawMode(false)
3087
+ }
3088
+ input.pause()
3089
+ output.write('\x1b[?25h')
3090
+ printLine('')
3091
+ }
3092
+
3093
+ function onKeypress(_chunk: string, key: KeypressInfo) {
3094
+ if (key.ctrl && key.name === 'c') {
3095
+ cleanup()
3096
+ rejectPromise(new Error('Setup cancelled.'))
3097
+ return
3098
+ }
3099
+ if (key.name === 'escape') {
3100
+ cleanup()
3101
+ rejectPromise(new Error('Setup cancelled.'))
3102
+ return
3103
+ }
3104
+ if (key.name === 'up') {
3105
+ selected = (selected - 1 + items.length) % items.length
3106
+ render()
3107
+ return
3108
+ }
3109
+ if (key.name === 'down') {
3110
+ selected = (selected + 1) % items.length
3111
+ render()
3112
+ return
3113
+ }
3114
+ if (key.name === 'return' || key.name === 'enter') {
3115
+ const item = items[selected]
3116
+ cleanup()
3117
+ resolvePromise(item)
3118
+ }
3119
+ }
3120
+
3121
+ input.on('keypress', onKeypress)
3122
+ input.resume()
3123
+ rawInput.setRawMode?.(true)
3124
+ })
3125
+ }
3126
+
3127
+ async function promptKeyboardMultiSelect<T>(
3128
+ title: string,
3129
+ items: T[],
3130
+ label: (item: T, index: number) => string,
3131
+ defaultSelected: (item: T, index: number) => boolean
3132
+ ): Promise<T[]> {
3133
+ if (items.length === 0) {
3134
+ return []
3135
+ }
3136
+ if (!isInteractiveTerminal()) {
3137
+ throw new Error(`${title} selection requires interactive terminal. Provide flags explicitly instead.`)
3138
+ }
3139
+
3140
+ printLine(`\n${sanitizeTerminalText(title)}`)
3141
+ printLine('Use ↑/↓ to move, Space to toggle, Enter to confirm, or Esc to skip.')
3142
+
3143
+ let selected = 0
3144
+ let rendered = false
3145
+ const checked = new Set<number>()
3146
+ items.forEach((item, index) => {
3147
+ if (defaultSelected(item, index)) {
3148
+ checked.add(index)
3149
+ }
3150
+ })
3151
+ const rawInput = input as RawInput
3152
+ const wasRaw = Boolean(rawInput.isRaw)
3153
+ emitKeypressEvents(input)
3154
+
3155
+ function render() {
3156
+ if (rendered) {
3157
+ output.write(`\x1b[${items.length}A`)
3158
+ }
3159
+ items.forEach((item, index) => {
3160
+ output.write('\x1b[2K')
3161
+ const cursor = index === selected ? '›' : ' '
3162
+ const marker = checked.has(index) ? '◉' : '○'
3163
+ output.write(`${cursor} ${marker} ${sanitizeTerminalText(label(item, index))}\n`)
3164
+ })
3165
+ rendered = true
3166
+ }
3167
+
3168
+ render()
3169
+ output.write('\x1b[?25l')
3170
+
3171
+ return new Promise((resolvePromise, rejectPromise) => {
3172
+ function cleanup() {
3173
+ input.off('keypress', onKeypress)
3174
+ if (rawInput.setRawMode && !wasRaw) {
3175
+ rawInput.setRawMode(false)
3176
+ }
3177
+ input.pause()
3178
+ output.write('\x1b[?25h')
3179
+ printLine('')
3180
+ }
3181
+
3182
+ function onKeypress(_chunk: string, key: KeypressInfo) {
3183
+ if (key.ctrl && key.name === 'c') {
3184
+ cleanup()
3185
+ rejectPromise(new Error('Setup cancelled.'))
3186
+ return
3187
+ }
3188
+ if (key.name === 'escape') {
3189
+ cleanup()
3190
+ resolvePromise([])
3191
+ return
3192
+ }
3193
+ if (key.name === 'up') {
3194
+ selected = (selected - 1 + items.length) % items.length
3195
+ render()
3196
+ return
3197
+ }
3198
+ if (key.name === 'down') {
3199
+ selected = (selected + 1) % items.length
3200
+ render()
3201
+ return
3202
+ }
3203
+ if (key.name === 'space') {
3204
+ if (checked.has(selected)) {
3205
+ checked.delete(selected)
3206
+ } else {
3207
+ checked.add(selected)
3208
+ }
3209
+ render()
3210
+ return
3211
+ }
3212
+ if (key.name === 'return' || key.name === 'enter') {
3213
+ const selectedItems = items.filter((_item, index) => checked.has(index))
3214
+ cleanup()
3215
+ resolvePromise(selectedItems)
3216
+ }
3217
+ }
3218
+
3219
+ input.on('keypress', onKeypress)
3220
+ input.resume()
3221
+ rawInput.setRawMode?.(true)
3222
+ })
3223
+ }
3224
+
3225
+ function formatFindingCount(count: number, label: string): string {
3226
+ return `${count} ${label}${count === 1 ? '' : 's'}`
3227
+ }
3228
+
3229
+ function validationSummary(findings: { severity: string }[], logPath: string | null): string {
3230
+ if (findings.length === 0) {
3231
+ return 'passed'
3232
+ }
3233
+
3234
+ const errors = findings.filter(finding => finding.severity === 'error').length
3235
+ const warnings = findings.filter(finding => finding.severity === 'warning').length
3236
+ const infos = findings.filter(finding => finding.severity === 'info').length
3237
+ const parts = [
3238
+ errors > 0 ? formatFindingCount(errors, 'error') : null,
3239
+ warnings > 0 ? formatFindingCount(warnings, 'warning') : null,
3240
+ infos > 0 ? formatFindingCount(infos, 'info') : null,
3241
+ ].filter(Boolean)
3242
+ const location = logPath ? ` (printed to ${logPath})` : ''
3243
+ return `${parts.join(', ')}${location}`
3244
+ }
3245
+
3246
+ function writeWorkspaceValidationLog(
3247
+ workspaceRoot: string,
3248
+ findings: { severity: string, code: string, path?: string, message: string }[],
3249
+ dryRun: boolean
3250
+ ): string | null {
3251
+ if (findings.length === 0 || dryRun) {
3252
+ return null
3253
+ }
3254
+
3255
+ const logsDir = join(workspaceRoot, '.logs')
3256
+ mkdirSync(logsDir, { recursive: true })
3257
+ const logPath = join(logsDir, `${randomBytes(6).toString('hex')}.log`)
3258
+ const lines = [
3259
+ 'Orizu workspace validation',
3260
+ `Generated at: ${new Date().toISOString()}`,
3261
+ `Workspace: ${workspaceRoot}`,
3262
+ `Summary: ${validationSummary(findings, null)}`,
3263
+ '',
3264
+ ...findings.map(finding => {
3265
+ const path = finding.path ? ` ${finding.path}` : ''
3266
+ return `${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`
3267
+ }),
3268
+ '',
3269
+ ]
3270
+ writeFileSync(logPath, lines.join('\n'), 'utf8')
3271
+ return relative(workspaceRoot, logPath)
3272
+ }
3273
+
3274
+ function printWorkspaceFindings(findings: { severity: string, code: string, path?: string, message: string }[]) {
3275
+ if (findings.length === 0) {
3276
+ printLine(' Validation passed.')
3277
+ return
3278
+ }
3279
+
3280
+ printLine(' Validation findings:')
3281
+ for (const finding of findings) {
3282
+ const path = finding.path ? ` ${finding.path}` : ''
3283
+ printLine(` - ${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`)
3284
+ }
3285
+ }
3286
+
2924
3287
  const SKILL_INSTALL_USAGE =
2925
3288
  `Usage: orizu install-skill [--agent <${SKILL_INSTALL_AGENTS.join('|')}>]... [--scope global|project] [--mode auto|link|copy] [--target <${SKILL_INSTALL_TARGETS.join('|')}>]... [--yes] [--dry-run]`
2926
3289
 
@@ -2933,6 +3296,7 @@ interface SkillInstallChoice {
2933
3296
  function describeSkillTarget(target: SkillInstallTarget): string {
2934
3297
  if (target === 'claude-user') return 'Claude Code, for you across all projects'
2935
3298
  if (target === 'claude-project') return 'Claude Code, only for this project'
3299
+ if (target === 'codex-user') return 'Codex, for you across all projects'
2936
3300
  if (target === 'agent-user') return 'Codex / Open Agent Skills, for you across all projects'
2937
3301
  if (target === 'agents-project') return 'Codex / Open Agent Skills, only for this project'
2938
3302
  if (target === 'codex-project') return 'Codex legacy project folder (.codex/skills)'
@@ -3053,15 +3417,31 @@ interface SkillInstallOutcome {
3053
3417
 
3054
3418
  async function applySkillInstallTargets(
3055
3419
  targets: SkillInstallTarget[],
3056
- options: { mode: SkillInstallMode, skipConfirm: boolean, dryRun: boolean }
3420
+ options: {
3421
+ mode: SkillInstallMode
3422
+ skipConfirm: boolean
3423
+ dryRun: boolean
3424
+ cwd?: string
3425
+ homeDir?: string
3426
+ showPlan?: boolean
3427
+ printResults?: boolean
3428
+ }
3057
3429
  ): Promise<SkillInstallOutcome[]> {
3058
3430
  const cliVersion = getCliVersion()
3059
3431
  const jsonMode = hasJsonFlag()
3060
3432
  const planned = targets.map(target =>
3061
- installSkillTarget(target, { dryRun: true, mode: options.mode, cliVersion })
3433
+ installSkillTarget(target, {
3434
+ cwd: options.cwd,
3435
+ homeDir: options.homeDir,
3436
+ dryRun: true,
3437
+ mode: options.mode,
3438
+ cliVersion,
3439
+ })
3062
3440
  )
3441
+ const showPlan = options.showPlan ?? true
3442
+ const printResults = options.printResults ?? true
3063
3443
 
3064
- if (!jsonMode) {
3444
+ if (showPlan && !jsonMode) {
3065
3445
  printLine('Planned writes:')
3066
3446
  for (const plan of planned) {
3067
3447
  printLine(describePlannedWrite(plan.target, plan.action, plan.mode, plan.path))
@@ -3070,7 +3450,7 @@ async function applySkillInstallTargets(
3070
3450
  }
3071
3451
 
3072
3452
  if (options.dryRun) {
3073
- if (!jsonMode) {
3453
+ if (printResults && !jsonMode) {
3074
3454
  printLine('Dry run: no files were changed.')
3075
3455
  }
3076
3456
  return planned.map(plan => ({
@@ -3084,8 +3464,8 @@ async function applySkillInstallTargets(
3084
3464
  const outcomes: SkillInstallOutcome[] = []
3085
3465
  for (const target of targets) {
3086
3466
  let overwrite = options.skipConfirm
3087
- const installPath = getSkillInstallPath(target)
3088
- if (!overwrite && targetNeedsOverwrite(target)) {
3467
+ const installPath = getSkillInstallPath(target, { cwd: options.cwd, homeDir: options.homeDir })
3468
+ if (!overwrite && targetNeedsOverwrite(target, { cwd: options.cwd, homeDir: options.homeDir })) {
3089
3469
  if (!isInteractiveTerminal()) {
3090
3470
  throw new Error(`${installPath} already exists. Pass --yes to replace it.`)
3091
3471
  }
@@ -3101,11 +3481,13 @@ async function applySkillInstallTargets(
3101
3481
 
3102
3482
  try {
3103
3483
  const result = installSkillTarget(target, {
3484
+ cwd: options.cwd,
3485
+ homeDir: options.homeDir,
3104
3486
  overwrite,
3105
3487
  mode: options.mode,
3106
3488
  cliVersion,
3107
3489
  })
3108
- if (!jsonMode) {
3490
+ if (printResults && !jsonMode) {
3109
3491
  const suffix = result.mode === 'link' ? ' (symlink)' : ''
3110
3492
  printLine(`${formatSkillInstallAction(result.action)} ${result.path}${suffix}`)
3111
3493
  }
@@ -3115,9 +3497,9 @@ async function applySkillInstallTargets(
3115
3497
  action: result.action as 'created' | 'updated',
3116
3498
  mode: result.mode,
3117
3499
  })
3118
- } catch (error: any) {
3119
- const message = error?.message || String(error)
3120
- if (!jsonMode) {
3500
+ } catch (error: unknown) {
3501
+ const message = getErrorMessage(error)
3502
+ if (printResults && !jsonMode) {
3121
3503
  printLine(`Failed ${installPath}: ${message}`)
3122
3504
  }
3123
3505
  outcomes.push({ target, path: installPath, action: 'failed', mode: null, error: message })
@@ -3295,17 +3677,161 @@ function describeSetupAuthState(): { state: 'signed-in' | 'signed-out', baseUrl:
3295
3677
  }
3296
3678
  }
3297
3679
 
3680
+ type SetupTeamChoice =
3681
+ | { kind: 'team', team: Team }
3682
+ | { kind: 'create' }
3683
+
3684
+ async function resolveSetupTeam(teamSlugArg: string | null, noInput: boolean, dryRun: boolean): Promise<Team> {
3685
+ const teams = await fetchTeams()
3686
+ const normalizedTeamSlug = teamSlugArg ? normalizeSlugInput(teamSlugArg) : null
3687
+
3688
+ if (normalizedTeamSlug) {
3689
+ const team = teams.find(candidate => candidate.slug === normalizedTeamSlug)
3690
+ if (!team) {
3691
+ throw new Error(`Team '${normalizedTeamSlug}' was not found in your accessible teams.`)
3692
+ }
3693
+ return team
3694
+ }
3695
+
3696
+ if (noInput || dryRun) {
3697
+ throw new Error('Authenticated setup requires --team <slug> when running non-interactively.')
3698
+ }
3699
+
3700
+ const choices: SetupTeamChoice[] = [
3701
+ ...teams.map(team => ({ kind: 'team' as const, team })),
3702
+ { kind: 'create' },
3703
+ ]
3704
+ const choice = await promptKeyboardSelect(
3705
+ 'Choose the team to set up in this directory',
3706
+ choices,
3707
+ item => item.kind === 'create'
3708
+ ? 'Create a new team'
3709
+ : `${item.team.name} (${item.team.slug})`
3710
+ )
3711
+
3712
+ if (choice.kind === 'team') {
3713
+ return choice.team
3714
+ }
3715
+
3716
+ const name = await askText(' Team name?', '')
3717
+ if (!name) {
3718
+ throw new Error('Team name is required to create a team.')
3719
+ }
3720
+ return createTeamOnServer(name)
3721
+ }
3722
+
3723
+ async function resolveSetupProjects(team: Team): Promise<Project[]> {
3724
+ const projects = await fetchProjects(team.slug)
3725
+
3726
+ if (projects.length === 0) {
3727
+ throw new Error(`Team '${team.slug}' has no projects. Create a project before running setup.`)
3728
+ }
3729
+
3730
+ return projects
3731
+ }
3732
+
3733
+ function projectSeedsFromProjects(projects: Project[]): WorkspaceProjectSeed[] {
3734
+ return projects.map(project => ({
3735
+ slug: project.slug,
3736
+ id: project.id,
3737
+ name: project.name,
3738
+ }))
3739
+ }
3740
+
3741
+ function formatProjectSetupProgress(count: number): string {
3742
+ return `${count} project${count === 1 ? '' : 's'} being set up in workspace...`
3743
+ }
3744
+
3745
+ interface SetupSkillChoice {
3746
+ agent: SkillInstallAgent
3747
+ label: string
3748
+ target: SkillInstallTarget
3749
+ }
3750
+
3751
+ function setupTargetForAgent(agent: SkillInstallAgent): SkillInstallTarget {
3752
+ return agent === 'codex' ? 'codex-user' : 'claude-user'
3753
+ }
3754
+
3755
+ function resolveSetupSkillHome(): string {
3756
+ return resolve(process.env.HOME || homedir())
3757
+ }
3758
+
3759
+ function detectedSetupSkillChoices(homeDir: string): SetupSkillChoice[] {
3760
+ const choices: SetupSkillChoice[] = []
3761
+ if (existsSync(join(homeDir, '.codex'))) {
3762
+ choices.push({
3763
+ agent: 'codex',
3764
+ label: 'Codex',
3765
+ target: 'codex-user',
3766
+ })
3767
+ }
3768
+
3769
+ if (existsSync(join(homeDir, '.claude'))) {
3770
+ choices.push({
3771
+ agent: 'claude',
3772
+ label: 'Claude Code',
3773
+ target: 'claude-user',
3774
+ })
3775
+ }
3776
+
3777
+ return choices
3778
+ }
3779
+
3780
+ function setupSkillTargetsFromArgs(): SkillInstallTarget[] {
3781
+ const targets: SkillInstallTarget[] = []
3782
+ for (const agent of getArgs('--agent')) {
3783
+ if (!isSkillInstallAgent(agent)) {
3784
+ throw new Error(
3785
+ `Unknown agent '${agent}'. Available agents: ${SKILL_INSTALL_AGENTS.join(', ')}`
3786
+ )
3787
+ }
3788
+ const target = setupTargetForAgent(agent)
3789
+ if (!targets.includes(target)) {
3790
+ targets.push(target)
3791
+ }
3792
+ }
3793
+ for (const target of getArgs('--target')) {
3794
+ if (!isSkillInstallTarget(target)) {
3795
+ throw new Error(
3796
+ `Unknown skill install target '${target}'. Available targets: ${SKILL_INSTALL_TARGETS.join(', ')}`
3797
+ )
3798
+ }
3799
+ if (!targets.includes(target)) {
3800
+ targets.push(target)
3801
+ }
3802
+ }
3803
+ return targets
3804
+ }
3805
+
3806
+ function setupSkillLabel(target: SkillInstallTarget): string {
3807
+ if (target === 'codex-user' || target === 'codex-project' || target === 'agent-user' || target === 'agents-project') {
3808
+ return 'Codex skill'
3809
+ }
3810
+ if (target === 'claude-user' || target === 'claude-project') return 'Claude Code skill'
3811
+ return 'Orizu skill'
3812
+ }
3813
+
3298
3814
  async function setupCommand() {
3299
3815
  const dryRun = hasArg('--dry-run')
3816
+ const validateOnly = hasArg('--validate') && !hasArg('--fix')
3817
+ const fix = hasArg('--fix')
3818
+ const verbose = hasArg('--verbose')
3819
+ const handoffRequested = hasArg('--handoff') || Boolean(getArg('--launch'))
3300
3820
  const skipConfirm = hasArg('--yes')
3301
- const noInput = hasArg('--no-input') || !isInteractiveTerminal()
3821
+ const noInput = hasArg('--no-input') || hasArg('--non-interactive') || !isInteractiveTerminal()
3822
+ const workspaceArg = getOptionalArgValue('--workspace')
3823
+ const workspaceRoot = workspaceArg ? resolve(expandHomePath(workspaceArg)) : process.cwd()
3824
+ const teamArg = getOptionalArgValue('--team')
3825
+ const projectArg = getOptionalArgValue('--project')
3826
+ const setupSkillHome = resolveSetupSkillHome()
3827
+ const installMode: SkillInstallMode = getArg('--mode') ? parseSkillInstallMode() : 'link'
3302
3828
 
3303
3829
  printBannerIfInteractive()
3304
3830
  printLine('Orizu setup')
3305
3831
  printLine('')
3306
3832
 
3307
- // Phase 1: login
3308
- printLine('1. Authentication')
3833
+ // Step 1: login
3834
+ printLine('Step 1: Login')
3309
3835
  let auth = describeSetupAuthState()
3310
3836
  if (hasArg('--skip-login')) {
3311
3837
  printLine(' Skipped (--skip-login).')
@@ -3313,70 +3839,159 @@ async function setupCommand() {
3313
3839
  printLine(` Already authenticated with ${sanitizeTerminalText(auth.baseUrl)}.`)
3314
3840
  } else if (noInput || dryRun) {
3315
3841
  printLine(' Not signed in. Run `orizu login` to authenticate.')
3316
- } else if (await askYesNo(' Sign in to Orizu now?', true)) {
3842
+ } else if (await waitForAnyKeyOrEscape(' Press any key to open a browser and connect to your account, or Esc to cancel.')) {
3317
3843
  await login()
3318
3844
  auth = describeSetupAuthState()
3319
3845
  } else {
3320
- printLine(' Skipped. Run `orizu login` when ready.')
3321
- }
3322
- printLine('')
3323
-
3324
- // Phase 2: agent integration
3325
- printLine('2. Agent integration')
3326
- const scope = parseSkillInstallScope()
3327
- const mode = parseSkillInstallMode()
3328
- let targets = hasArg('--no-install') ? [] : parseSkillInstallTargets(scope)
3329
- if (targets.length === 0 && !hasArg('--no-install')) {
3330
- if (noInput) {
3331
- printLine(' No agents selected. Pass --agent claude and/or --agent codex, or rerun interactively.')
3332
- } else {
3333
- targets = await promptSkillInstallTargets(scope)
3334
- }
3335
- }
3336
-
3337
- let installOutcomes: SkillInstallOutcome[] = []
3338
- if (targets.length > 0) {
3339
- installOutcomes = await applySkillInstallTargets(targets, { mode, skipConfirm, dryRun })
3340
- } else if (hasArg('--no-install')) {
3341
- printLine(' Skipped (--no-install).')
3846
+ throw new Error('Setup cancelled.')
3342
3847
  }
3343
3848
  printLine('')
3344
3849
 
3345
- // Phase 3: local workspace
3346
- printLine('3. Local workspace')
3347
- let workspaceState: 'created' | 'exists' | 'skipped' | 'would-create' = 'skipped'
3348
- if (hasArg('--no-workspace')) {
3850
+ // Step 2: local workspace
3851
+ printLine('Step 2: Setup your workspace')
3852
+ let workspaceState: 'created' | 'exists' | 'skipped' | 'would-create' | 'validated' | 'invalid' | 'repaired' = 'skipped'
3853
+ let workspaceResult: ReturnType<typeof initOrizuWorkspace> | null = null
3854
+ let validationLogPath: string | null = null
3855
+ let hostedAttached = false
3856
+ if (hasArg('--no-workspace') && !validateOnly && !fix) {
3349
3857
  printLine(' Skipped (--no-workspace).')
3350
- } else if (workspaceExists()) {
3351
- workspaceState = 'exists'
3352
- printLine(` ${WORKSPACE_DIR_NAME}/ already initialized.`)
3353
3858
  } else {
3354
- const wantsWorkspace = hasArg('--workspace') ||
3355
- (!noInput && await askYesNo(` Create a gitignored ${WORKSPACE_DIR_NAME}/ workspace for local Orizu artifacts?`, true))
3859
+ const wantsWorkspace = validateOnly
3860
+ || fix
3861
+ || hasArg('--workspace')
3862
+ || Boolean(teamArg || projectArg)
3863
+ || workspaceExists(workspaceRoot)
3864
+ || !noInput
3356
3865
  if (wantsWorkspace) {
3866
+ let teamSlug = teamArg
3867
+ const projectSlug = projectArg
3868
+ let teamId: string | null = null
3869
+ let projects: WorkspaceProjectSeed[] | undefined
3870
+
3871
+ if (auth.state === 'signed-in' && !validateOnly) {
3872
+ const team = await resolveSetupTeam(teamSlug, noInput, dryRun)
3873
+ const serverProjects = await resolveSetupProjects(team)
3874
+ teamSlug = team.slug
3875
+ teamId = team.id
3876
+ projects = projectSeedsFromProjects(serverProjects)
3877
+ printLine(` ${formatProjectSetupProgress(projects.length)}`)
3878
+ if (!hasArg('--local') && !noInput && !dryRun) { let link: GithubLinkResult | null = null; try { link = await runGithubLink(team.slug, { print: printLine, fetcher: authedFetch, openUrl: openInBrowser, confirm: (org) => askYesNo(` Connect this team to GitHub org '${org}'?`, true) }) } catch (error) { printLine(` GitHub link skipped: ${getErrorMessage(error)} Run \`orizu github link --team ${team.slug}\` to finish.`) }
3879
+ if (link) { try { hostedAttached = (await runInteractiveHostedSetup({ teamSlug: team.slug, targetDir: workspaceRoot, local: hasArg('--local'), activeInstallation: link.status === 'active' }, { print: printLine, fetcher: authedFetch })).attached } catch (error) { throw new Error(`Hosted setup failed: ${getErrorMessage(error)} Remove any partial clone in ${workspaceRoot} if one exists, then re-run \`orizu setup\` (or pass --local to scaffold a local-only workspace).`) } } }
3880
+ } else if (!teamSlug && !noInput && !validateOnly) {
3881
+ teamSlug = await askText(' Team slug?', 'local-team')
3882
+ }
3883
+ if (auth.state !== 'signed-in' && (!teamSlug || !projectSlug) && noInput && hasArg('--workspace')) {
3884
+ printLine(' Using starter local-only team/project. Pass --team with authentication to materialize server projects.')
3885
+ }
3886
+ if (projectSlug && auth.state !== 'signed-in') {
3887
+ projects = [{ slug: projectSlug }]
3888
+ }
3889
+ if (auth.state !== 'signed-in' && !validateOnly) {
3890
+ const localProjectCount = projects?.length || 1
3891
+ printLine(` ${formatProjectSetupProgress(localProjectCount)}`)
3892
+ }
3893
+
3894
+ if (hostedAttached) { workspaceState = 'exists' } else {
3357
3895
  const result = initOrizuWorkspace({
3896
+ workspaceRoot,
3897
+ teamSlug,
3898
+ teamId,
3899
+ projectSlug: auth.state === 'signed-in' ? null : projectSlug,
3900
+ projects,
3358
3901
  baseUrl: auth.baseUrl,
3902
+ serviceOrigin: getOptionalArgValue('--service-origin') || auth.baseUrl,
3903
+ attachWorkspaceId: getOptionalArgValue('--attach-workspace'),
3359
3904
  cliVersion: getCliVersion(),
3360
3905
  dryRun,
3906
+ validateOnly,
3907
+ fix,
3908
+ noSymlinks: hasArg('--no-symlinks'),
3361
3909
  })
3362
- workspaceState = result.state === 'would-create' ? 'would-create' : result.state
3363
- for (const action of result.actions) {
3364
- printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`)
3910
+ workspaceResult = result
3911
+ workspaceState = result.state
3912
+ if (dryRun || verbose) {
3913
+ for (const action of result.actions) {
3914
+ printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`)
3915
+ }
3916
+ } else if (result.actions.length > 0) {
3917
+ printLine(validateOnly ? ' Workspace validation complete' : ' Workspace setup complete')
3918
+ } else if (!validateOnly) {
3919
+ printLine(' Workspace setup complete')
3365
3920
  }
3921
+ validationLogPath = writeWorkspaceValidationLog(result.root, result.findings, dryRun)
3922
+ if (verbose) {
3923
+ printWorkspaceFindings(result.findings)
3924
+ }
3925
+ if ((validateOnly || fix) && result.findings.some(finding => finding.severity === 'error')) {
3926
+ process.exitCode = 1
3927
+ }
3928
+ }
3929
+ } else {
3930
+ printLine(` Skipped. Rerun with --workspace to initialize the workspace contract.`)
3931
+ }
3932
+ }
3933
+ printLine('')
3934
+
3935
+ // Step 3: global coding-agent skills
3936
+ let installOutcomes: SkillInstallOutcome[] = []
3937
+ printLine('Step 3: Install coding agent skills')
3938
+ if (hasArg('--no-install') || validateOnly || fix) {
3939
+ printLine(hasArg('--no-install') ? ' Skipped (--no-install).' : ' Skipped for validation/repair.')
3940
+ } else {
3941
+ let targets = setupSkillTargetsFromArgs()
3942
+ if (targets.length === 0 && !noInput && !dryRun) {
3943
+ const choices = detectedSetupSkillChoices(setupSkillHome)
3944
+ if (choices.length > 0) {
3945
+ const selectedChoices = await promptKeyboardMultiSelect(
3946
+ 'Choose the coding agents that should learn how to use Orizu',
3947
+ choices,
3948
+ choice => choice.label,
3949
+ () => true
3950
+ )
3951
+ targets = selectedChoices.map(choice => choice.target)
3952
+ } else {
3953
+ printLine(' No Codex or Claude Code agent folders were detected.')
3954
+ }
3955
+ }
3956
+
3957
+ if (targets.length === 0) {
3958
+ printLine(' Skipped skill install.')
3366
3959
  } else {
3367
- printLine(` Skipped. Rerun with --workspace to create ${WORKSPACE_DIR_NAME}/.`)
3960
+ printLine(' Installing skills...')
3961
+ installOutcomes = await applySkillInstallTargets(targets, {
3962
+ mode: installMode,
3963
+ skipConfirm,
3964
+ dryRun,
3965
+ cwd: workspaceRoot,
3966
+ homeDir: setupSkillHome,
3967
+ showPlan: false,
3968
+ printResults: false,
3969
+ })
3970
+ if (dryRun) {
3971
+ printLine(' Dry run: no skill files were changed.')
3972
+ }
3973
+ for (const outcome of installOutcomes) {
3974
+ const label = setupSkillLabel(outcome.target)
3975
+ if (outcome.action === 'failed') {
3976
+ printLine(` ${label} failed: ${outcome.error}`)
3977
+ } else if (outcome.action === 'skipped') {
3978
+ printLine(` ${label} skipped`)
3979
+ } else {
3980
+ printLine(` ${label} installed`)
3981
+ }
3982
+ }
3368
3983
  }
3369
3984
  }
3370
3985
  printLine('')
3371
3986
 
3372
3987
  // Phase 4: coding-agent handoff
3373
3988
  const handoffPrompt = renderAgentSetupPrompt({
3374
- workspacePath: workspaceState === 'created' || workspaceState === 'exists'
3375
- ? getWorkspaceRoot()
3989
+ workspacePath: workspaceResult && workspaceState !== 'invalid'
3990
+ ? getWorkspaceRoot(workspaceRoot)
3376
3991
  : null,
3377
3992
  })
3378
- if (!hasArg('--no-handoff')) {
3379
- printLine('4. Coding-agent handoff')
3993
+ if (handoffRequested && !hasArg('--no-handoff')) {
3994
+ printLine('Step 4: Coding-agent handoff')
3380
3995
  printLine(' Give your coding agent this prompt to plan repo-specific Orizu adoption:')
3381
3996
  printLine('')
3382
3997
  printLine('--- prompt start ---')
@@ -3416,7 +4031,7 @@ async function setupCommand() {
3416
4031
  const summaryTargets = installOutcomes.length > 0
3417
4032
  ? installOutcomes
3418
4033
  .filter(outcome => outcome.action !== 'failed')
3419
- .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target) }))
4034
+ .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target, { cwd: workspaceRoot, homeDir: setupSkillHome }) }))
3420
4035
  : []
3421
4036
  const source = resolveSkillSource()
3422
4037
 
@@ -3436,7 +4051,21 @@ async function setupCommand() {
3436
4051
  cliVersion: getCliVersion(),
3437
4052
  skillHash: computeSkillContentHash(source.root),
3438
4053
  },
3439
- workspace: { state: workspaceState, path: getWorkspaceRoot() },
4054
+ workspace: {
4055
+ state: workspaceState,
4056
+ path: getWorkspaceRoot(workspaceRoot),
4057
+ validationLogPath,
4058
+ operations: workspaceResult?.operations.map(op => ({
4059
+ action: op.action,
4060
+ path: op.path,
4061
+ safe: op.safe,
4062
+ reason: op.reason,
4063
+ entries: op.entries,
4064
+ sourcePath: op.sourcePath,
4065
+ target: op.target,
4066
+ })) || [],
4067
+ findings: workspaceResult?.findings || [],
4068
+ },
3440
4069
  })
3441
4070
  return
3442
4071
  }
@@ -3444,21 +4073,23 @@ async function setupCommand() {
3444
4073
  printLine('Setup summary')
3445
4074
  printLine(` Auth: ${auth.state === 'signed-in' ? `signed in (${sanitizeTerminalText(auth.baseUrl)})` : 'not signed in — run `orizu login`'}`)
3446
4075
  if (summaryTargets.length > 0) {
3447
- printLine(' Integrations:')
4076
+ printLine(' Skills:')
3448
4077
  for (const { outcome, status } of summaryTargets) {
3449
4078
  const modeLabel = status.mode ? `, ${status.mode}` : ''
3450
4079
  printLine(` ${outcome.target.padEnd(16)} ${status.state}${modeLabel} ${outcome.path}`)
3451
4080
  }
3452
4081
  } else {
3453
- printLine(' Integrations: none installed this run — `orizu install-skill` sets them up.')
4082
+ printLine(' Skills: none installed this run')
3454
4083
  }
3455
4084
  const failed = installOutcomes.filter(outcome => outcome.action === 'failed')
3456
4085
  for (const failure of failed) {
3457
4086
  printLine(` ${failure.target.padEnd(16)} failed: ${failure.error}`)
3458
4087
  }
3459
- printLine(` Skill source: ${source.root} (${source.source})`)
3460
- printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : `${getWorkspaceRoot()} (${workspaceState})`}`)
3461
- printLine(' Next: hand the prompt above to your coding agent to plan repo-specific Orizu adoption.')
4088
+ printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : getWorkspaceRoot(workspaceRoot)}`)
4089
+ if (workspaceResult) {
4090
+ printLine(` Validation: ${validationSummary(workspaceResult.findings, validationLogPath)}`)
4091
+ }
4092
+ printLine(' Next: run `orizu setup prompt` if you want a coding-agent handoff prompt.')
3462
4093
  }
3463
4094
 
3464
4095
  function setupPromptCommand() {
@@ -3481,8 +4112,8 @@ function readJsonFile(pathArg: string): Record<string, unknown> {
3481
4112
  }
3482
4113
 
3483
4114
  return parsed as Record<string, unknown>
3484
- } catch (error: any) {
3485
- throw new Error(`Invalid JSON file '${pathArg}': ${error?.message || String(error)}`)
4115
+ } catch (error: unknown) {
4116
+ throw new Error(`Invalid JSON file '${pathArg}': ${getErrorMessage(error)}`)
3486
4117
  }
3487
4118
  }
3488
4119
 
@@ -3559,141 +4190,6 @@ function writeTextFileEnsuringDir(pathArg: string, content: string) {
3559
4190
  writeFileSync(pathArg, content)
3560
4191
  }
3561
4192
 
3562
- function shouldExcludeArtifactPath(relativePath: string): boolean {
3563
- const parts = relativePath.split('/')
3564
- return parts.some(part =>
3565
- part === '.git' ||
3566
- part === '.DS_Store' ||
3567
- part === '__pycache__' ||
3568
- part === '.pytest_cache'
3569
- )
3570
- }
3571
-
3572
- function collectArtifactFiles(sourceDir: string, relativeDir = ''): string[] {
3573
- const absoluteDir = relativeDir ? join(sourceDir, relativeDir) : sourceDir
3574
- return readdirSync(absoluteDir, { withFileTypes: true })
3575
- .flatMap(entry => {
3576
- const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name
3577
- if (shouldExcludeArtifactPath(relativePath)) {
3578
- return []
3579
- }
3580
- if (entry.isDirectory()) {
3581
- return collectArtifactFiles(sourceDir, relativePath)
3582
- }
3583
- return entry.isFile() ? [relativePath] : []
3584
- })
3585
- .sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
3586
- }
3587
-
3588
- const ZIP_GENERAL_PURPOSE_UTF8 = 0x0800
3589
- const ZIP_DOS_TIME_2000_01_01 = 0
3590
- const ZIP_DOS_DATE_2000_01_01 = ((2000 - 1980) << 9) | (1 << 5) | 1
3591
- const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
3592
- let value = index
3593
- for (let bit = 0; bit < 8; bit++) {
3594
- value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1)
3595
- }
3596
- return value >>> 0
3597
- })
3598
-
3599
- function crc32(bytes: Buffer): number {
3600
- let crc = 0xffffffff
3601
- for (const byte of bytes) {
3602
- crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8)
3603
- }
3604
- return (crc ^ 0xffffffff) >>> 0
3605
- }
3606
-
3607
- function createStoredZip(entries: Array<{ path: string; data: Buffer }>): Buffer {
3608
- const localParts: Buffer[] = []
3609
- const centralParts: Buffer[] = []
3610
- let offset = 0
3611
-
3612
- for (const entry of entries) {
3613
- const name = Buffer.from(entry.path, 'utf8')
3614
- const crc = crc32(entry.data)
3615
- const localHeader = Buffer.alloc(30)
3616
- localHeader.writeUInt32LE(0x04034b50, 0)
3617
- localHeader.writeUInt16LE(10, 4)
3618
- localHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 6)
3619
- localHeader.writeUInt16LE(0, 8)
3620
- localHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 10)
3621
- localHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 12)
3622
- localHeader.writeUInt32LE(crc, 14)
3623
- localHeader.writeUInt32LE(entry.data.length, 18)
3624
- localHeader.writeUInt32LE(entry.data.length, 22)
3625
- localHeader.writeUInt16LE(name.length, 26)
3626
- localHeader.writeUInt16LE(0, 28)
3627
-
3628
- localParts.push(localHeader, name, entry.data)
3629
-
3630
- const centralHeader = Buffer.alloc(46)
3631
- centralHeader.writeUInt32LE(0x02014b50, 0)
3632
- centralHeader.writeUInt16LE(20, 4)
3633
- centralHeader.writeUInt16LE(10, 6)
3634
- centralHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 8)
3635
- centralHeader.writeUInt16LE(0, 10)
3636
- centralHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 12)
3637
- centralHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 14)
3638
- centralHeader.writeUInt32LE(crc, 16)
3639
- centralHeader.writeUInt32LE(entry.data.length, 20)
3640
- centralHeader.writeUInt32LE(entry.data.length, 24)
3641
- centralHeader.writeUInt16LE(name.length, 28)
3642
- centralHeader.writeUInt16LE(0, 30)
3643
- centralHeader.writeUInt16LE(0, 32)
3644
- centralHeader.writeUInt16LE(0, 34)
3645
- centralHeader.writeUInt16LE(0, 36)
3646
- centralHeader.writeUInt32LE(0, 38)
3647
- centralHeader.writeUInt32LE(offset, 42)
3648
- centralParts.push(centralHeader, name)
3649
-
3650
- offset += localHeader.length + name.length + entry.data.length
3651
- }
3652
-
3653
- const centralDirectoryOffset = offset
3654
- const centralDirectory = Buffer.concat(centralParts)
3655
- const endOfCentralDirectory = Buffer.alloc(22)
3656
- endOfCentralDirectory.writeUInt32LE(0x06054b50, 0)
3657
- endOfCentralDirectory.writeUInt16LE(0, 4)
3658
- endOfCentralDirectory.writeUInt16LE(0, 6)
3659
- endOfCentralDirectory.writeUInt16LE(entries.length, 8)
3660
- endOfCentralDirectory.writeUInt16LE(entries.length, 10)
3661
- endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12)
3662
- endOfCentralDirectory.writeUInt32LE(centralDirectoryOffset, 16)
3663
- endOfCentralDirectory.writeUInt16LE(0, 20)
3664
-
3665
- return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory])
3666
- }
3667
-
3668
- function zipDirectoryToBase64(dirArg: string): { zipBase64: string; contentSha256: string } {
3669
- const sourceDir = expandHomePath(dirArg)
3670
- try {
3671
- const stats = statSync(sourceDir)
3672
- if (!stats.isDirectory()) {
3673
- throw new Error(`${sourceDir} is not a directory`)
3674
- }
3675
- } catch (error: any) {
3676
- if (error?.code === 'ENOENT') {
3677
- throw new Error(`Directory not found: ${sourceDir}`)
3678
- }
3679
- throw error
3680
- }
3681
-
3682
- const files = collectArtifactFiles(sourceDir)
3683
- if (files.length === 0) {
3684
- throw new Error(`Directory contains no artifact files: ${sourceDir}`)
3685
- }
3686
-
3687
- const bytes = createStoredZip(files.map(relativePath => ({
3688
- path: relativePath,
3689
- data: readFileSync(join(sourceDir, relativePath)),
3690
- })))
3691
- return {
3692
- zipBase64: bytes.toString('base64'),
3693
- contentSha256: createHash('sha256').update(bytes).digest('hex'),
3694
- }
3695
- }
3696
-
3697
4193
  function readManifestFile(dirArg: string): Record<string, unknown> {
3698
4194
  return readJsonFile(join(expandHomePath(dirArg), 'manifest.json'))
3699
4195
  }
@@ -3909,13 +4405,47 @@ function parseCommaSeparated(value: string | null): string[] {
3909
4405
  .filter(Boolean)
3910
4406
  }
3911
4407
 
3912
- function readAssignmentManifest(path: string | null): ReturnType<typeof parseAssignmentManifestJsonl> | null {
4408
+ function readAssignmentManifest(path: string | null): ReturnType<typeof readAssignmentManifestJsonlFile> | null {
3913
4409
  if (!path) {
3914
4410
  return null
3915
4411
  }
3916
4412
 
3917
- const content = readFileSync(expandHomePath(path), 'utf8')
3918
- return parseAssignmentManifestJsonl(content)
4413
+ return readAssignmentManifestJsonlFile(expandHomePath(path))
4414
+ }
4415
+
4416
+ function readOptionalTaskTextArg(
4417
+ valueFlag: string,
4418
+ fileFlag: string
4419
+ ): string | undefined {
4420
+ const value = getArg(valueFlag)
4421
+ const file = getArg(fileFlag)
4422
+ rejectDashPrefixedOptionValue(valueFlag, value)
4423
+ rejectDashPrefixedOptionValue(fileFlag, file)
4424
+
4425
+ if (value !== null && file !== null) {
4426
+ throw new Error(`Use either ${valueFlag} or ${fileFlag}, not both.`)
4427
+ }
4428
+
4429
+ if (file !== null) {
4430
+ return readSourceFile(file)
4431
+ }
4432
+
4433
+ return value ?? undefined
4434
+ }
4435
+
4436
+ function readPositiveIntegerFlag(name: string): number | undefined {
4437
+ const value = getArg(name)
4438
+ rejectDashPrefixedOptionValue(name, value)
4439
+ if (value === null) {
4440
+ return undefined
4441
+ }
4442
+
4443
+ const parsed = Number(value)
4444
+ if (!Number.isInteger(parsed) || parsed < 1) {
4445
+ throw new Error(`${name} must be a positive integer`)
4446
+ }
4447
+
4448
+ return parsed
3919
4449
  }
3920
4450
 
3921
4451
  async function createTask() {
@@ -4058,6 +4588,155 @@ async function createTask() {
4058
4588
  )
4059
4589
  }
4060
4590
 
4591
+ async function updateTask() {
4592
+ const taskId = getArg('--task')
4593
+ rejectDashPrefixedOptionValue('--task', taskId)
4594
+ if (!taskId) {
4595
+ throw new Error('Usage: orizu tasks update --task <taskId> [--title <text>] [--description <text>|--description-file <path>] [--instructions <text>|--instructions-file <path>] [--dataset <datasetId>] [--app <appId> [--version <n>]] [--labels-per-item <n>] [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--json]')
4596
+ }
4597
+
4598
+ const title = getArg('--title')
4599
+ const description = readOptionalTaskTextArg('--description', '--description-file')
4600
+ const instructions = readOptionalTaskTextArg('--instructions', '--instructions-file')
4601
+ const datasetId = getArg('--dataset')
4602
+ const appId = getArg('--app')
4603
+ const versionNum = readPositiveIntegerFlag('--version')
4604
+ const requiredAssignmentsPerRow = readPositiveIntegerFlag('--labels-per-item')
4605
+ const assignees = parseCommaSeparated(getArg('--assignees'))
4606
+ const assignmentFile = getArg('--assignment-file')
4607
+
4608
+ rejectDashPrefixedOptionValue('--title', title)
4609
+ rejectDashPrefixedOptionValue('--dataset', datasetId)
4610
+ rejectDashPrefixedOptionValue('--app', appId)
4611
+ rejectDashPrefixedOptionValue('--assignment-file', assignmentFile)
4612
+
4613
+ if (title !== null && title.trim().length === 0) {
4614
+ throw new Error('--title must be a non-empty string')
4615
+ }
4616
+
4617
+ if (assignmentFile && assignees.length > 0) {
4618
+ throw new Error('Use either --assignment-file or --assignees, not both.')
4619
+ }
4620
+
4621
+ if (hasArg('--assignees') && assignees.length === 0) {
4622
+ throw new Error('--assignees requires at least one user ID or email')
4623
+ }
4624
+
4625
+ if (versionNum !== undefined && !appId) {
4626
+ throw new Error('--version requires --app')
4627
+ }
4628
+
4629
+ const explicitAssignments = readAssignmentManifest(assignmentFile)
4630
+ const requestBody: Record<string, unknown> = {}
4631
+ if (title !== null) requestBody.title = title.trim()
4632
+ if (description !== undefined) requestBody.description = description
4633
+ if (instructions !== undefined) requestBody.instructions = instructions
4634
+ if (datasetId !== null) requestBody.datasetId = datasetId
4635
+ if (appId !== null) requestBody.appId = appId
4636
+ if (versionNum !== undefined) requestBody.versionNum = versionNum
4637
+ if (requiredAssignmentsPerRow !== undefined) {
4638
+ requestBody.requiredAssignmentsPerRow = requiredAssignmentsPerRow
4639
+ }
4640
+ if (explicitAssignments) {
4641
+ requestBody.explicitAssignments = explicitAssignments
4642
+ } else if (hasArg('--assignees')) {
4643
+ requestBody.memberIds = assignees
4644
+ }
4645
+
4646
+ if (Object.keys(requestBody).length === 0) {
4647
+ throw new Error('Usage: orizu tasks update --task <taskId> [--title <text>] [--description <text>|--description-file <path>] [--instructions <text>|--instructions-file <path>] [--dataset <datasetId>] [--app <appId> [--version <n>]] [--labels-per-item <n>] [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--json]')
4648
+ }
4649
+
4650
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}`, {
4651
+ method: 'PATCH',
4652
+ headers: { 'Content-Type': 'application/json' },
4653
+ body: JSON.stringify(requestBody),
4654
+ })
4655
+
4656
+ if (!response.ok) {
4657
+ throw new Error(`Failed to update task: ${await extractErrorMessage(response)}`)
4658
+ }
4659
+
4660
+ const data = await parseJsonResponse<{
4661
+ task: {
4662
+ id: string
4663
+ title?: string | null
4664
+ status?: string | null
4665
+ dataset_id?: string | null
4666
+ app_id?: string | null
4667
+ version_id?: string | null
4668
+ required_assignments_per_row?: number | null
4669
+ }
4670
+ assignmentMode?: string
4671
+ assignmentsCreated?: number
4672
+ }>(response, 'Task update')
4673
+
4674
+ if (hasJsonFlag()) {
4675
+ printJson({
4676
+ taskId: data.task.id,
4677
+ title: data.task.title ?? null,
4678
+ status: data.task.status ?? null,
4679
+ datasetId: data.task.dataset_id ?? null,
4680
+ appId: data.task.app_id ?? null,
4681
+ versionId: data.task.version_id ?? null,
4682
+ requiredAssignmentsPerRow: data.task.required_assignments_per_row ?? null,
4683
+ ...(data.assignmentMode ? { assignmentMode: data.assignmentMode } : {}),
4684
+ ...(data.assignmentsCreated !== undefined
4685
+ ? { assignmentsCreated: data.assignmentsCreated }
4686
+ : {}),
4687
+ })
4688
+ return
4689
+ }
4690
+
4691
+ printLine(
4692
+ `Updated draft task ${sanitizeTerminalText(data.task.title || data.task.id)} (${sanitizeTerminalText(data.task.id)})` +
4693
+ (data.task.status ? ` [${sanitizeTerminalText(data.task.status)}]` : '') +
4694
+ (data.task.dataset_id ? `\n Dataset ID: ${sanitizeTerminalText(data.task.dataset_id)}` : '') +
4695
+ (data.task.version_id ? `\n Version ID: ${sanitizeTerminalText(data.task.version_id)}` : '') +
4696
+ (data.task.required_assignments_per_row
4697
+ ? `\n Labels/row: ${data.task.required_assignments_per_row}`
4698
+ : '') +
4699
+ (data.assignmentMode ? `\n Mode: ${sanitizeTerminalText(data.assignmentMode === 'custom' ? 'custom row map' : 'auto distribute')}` : '') +
4700
+ (data.assignmentsCreated !== undefined ? `\n Assignments: ${data.assignmentsCreated}` : '')
4701
+ )
4702
+ }
4703
+
4704
+ async function discardTask() {
4705
+ const taskId = getArg('--task')
4706
+ rejectDashPrefixedOptionValue('--task', taskId)
4707
+ if (!taskId) {
4708
+ throw new Error('Usage: orizu tasks discard --task <taskId> [--yes] [--json]')
4709
+ }
4710
+
4711
+ if (!hasArg('--yes')) {
4712
+ if (!isInteractiveTerminal()) {
4713
+ throw new Error('Refusing to discard a draft in non-interactive mode without --yes')
4714
+ }
4715
+
4716
+ const confirmed = await askYesNo(`Discard draft task ${taskId}? This cannot be undone.`, false)
4717
+ if (!confirmed) {
4718
+ printLine('Discard cancelled.')
4719
+ return
4720
+ }
4721
+ }
4722
+
4723
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}`, {
4724
+ method: 'DELETE',
4725
+ })
4726
+
4727
+ if (!response.ok) {
4728
+ throw new Error(`Failed to discard draft task: ${await extractErrorMessage(response)}`)
4729
+ }
4730
+
4731
+ const data = await parseJsonResponse<{ success: boolean }>(response, 'Task discard')
4732
+ if (hasJsonFlag()) {
4733
+ printJson({ taskId, discarded: data.success })
4734
+ return
4735
+ }
4736
+
4737
+ printLine(`Discarded draft task ${sanitizeTerminalText(taskId)}.`)
4738
+ }
4739
+
4061
4740
  async function publishTask() {
4062
4741
  const taskId = getArg('--task')
4063
4742
  const assignees = parseCommaSeparated(getArg('--assignees'))
@@ -4236,11 +4915,12 @@ async function taskStatus() {
4236
4915
  printTaskStatusSummary(data)
4237
4916
  }
4238
4917
 
4239
- async function updateTaskStatus(targetStatus: 'paused' | 'active') {
4918
+ async function updateTaskStatus(targetStatus: 'paused' | 'active' | 'completed') {
4240
4919
  const taskId = getArg('--task')
4920
+ const labels = TASK_STATUS_ACTION_LABELS[targetStatus]
4921
+
4241
4922
  if (!taskId) {
4242
- const verb = targetStatus === 'paused' ? 'pause' : 'unpause'
4243
- throw new Error(`Usage: orizu tasks ${verb} --task <taskId>`)
4923
+ throw new Error(`Usage: orizu tasks ${labels.verb} --task <taskId>`)
4244
4924
  }
4245
4925
 
4246
4926
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`, {
@@ -4250,8 +4930,7 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
4250
4930
  })
4251
4931
 
4252
4932
  if (!response.ok) {
4253
- const verb = targetStatus === 'paused' ? 'pause' : 'unpause'
4254
- throw new Error(`Failed to ${verb} task: ${await response.text()}`)
4933
+ throw new Error(`Failed to ${labels.verb} task: ${await response.text()}`)
4255
4934
  }
4256
4935
 
4257
4936
  const data = await parseJsonResponse<{ task: { id: string; status: string } }>(response, 'Task status update')
@@ -4259,8 +4938,7 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
4259
4938
  printJson({ task: data.task })
4260
4939
  return
4261
4940
  }
4262
- const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused'
4263
- printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
4941
+ printLine(`${labels.pastTense} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
4264
4942
  }
4265
4943
 
4266
4944
  async function setTaskReport() {
@@ -4523,10 +5201,10 @@ async function changeTeamMemberRole() {
4523
5201
  const email = getArg('--email')
4524
5202
  const role = getArg('--role')
4525
5203
  if (!teamSlug || !email || !role) {
4526
- throw new Error('Usage: orizu teams members role --team <teamSlug> --email <email> --role <admin|member>')
5204
+ throw new Error('Usage: orizu teams members role --team <teamSlug> --email <email> --role <admin|curator|judge>')
4527
5205
  }
4528
- if (!['admin', 'member'].includes(role)) {
4529
- throw new Error('role must be one of: admin, member')
5206
+ if (!['admin', 'curator', 'judge'].includes(role)) {
5207
+ throw new Error('role must be one of: admin, curator, judge')
4530
5208
  }
4531
5209
 
4532
5210
  const members = await fetchTeamMembers(teamSlug)
@@ -5508,12 +6186,16 @@ async function runnersExec() {
5508
6186
  }
5509
6187
 
5510
6188
  const runnerOutput = JSON.parse(readFileSync(outputPath, 'utf8')) as Record<string, unknown>
6189
+ // Spread runner output first: the scorer/prompt context identity fields
6190
+ // (row_id, version ids, metric_key) are authoritative, never runner-overridable.
5511
6191
  resultLines.push(JSON.stringify({
6192
+ ...runnerOutput,
5512
6193
  row_id: row.id,
5513
6194
  prompt_version_id: context.prompt.promptVersionId,
5514
6195
  runner_version_id: context.prompt.runnerVersionId,
5515
- ...(scorerVersion ? { scorer_version_id: context.scorer?.versionId || scorerVersion } : {}),
5516
- ...runnerOutput,
6196
+ ...(scorerVersion
6197
+ ? { scorer_version_id: context.scorer?.versionId || scorerVersion, metric_key: context.scorer?.metricKey || 'score' }
6198
+ : {}),
5517
6199
  }))
5518
6200
  } finally {
5519
6201
  rmSync(tempDir, { recursive: true, force: true })
@@ -5879,6 +6561,16 @@ export async function main(rawArgs = process.argv.slice(2)) {
5879
6561
  return
5880
6562
  }
5881
6563
 
6564
+ if (command === 'tasks' && subcommand === 'update') {
6565
+ await updateTask()
6566
+ return
6567
+ }
6568
+
6569
+ if (command === 'tasks' && subcommand === 'discard') {
6570
+ await discardTask()
6571
+ return
6572
+ }
6573
+
5882
6574
  if (command === 'tasks' && subcommand === 'publish') {
5883
6575
  await publishTask()
5884
6576
  return
@@ -5903,15 +6595,9 @@ export async function main(rawArgs = process.argv.slice(2)) {
5903
6595
  return
5904
6596
  }
5905
6597
 
5906
- if (command === 'tasks' && subcommand === 'pause') {
5907
- await updateTaskStatus('paused')
5908
- return
5909
- }
5910
-
5911
- if (command === 'tasks' && subcommand === 'unpause') {
5912
- await updateTaskStatus('active')
5913
- return
5914
- }
6598
+ if (command === 'tasks' && subcommand === 'pause') { await updateTaskStatus('paused'); return }
6599
+ if (command === 'tasks' && subcommand === 'unpause') { await updateTaskStatus('active'); return }
6600
+ if (command === 'tasks' && subcommand === 'complete') { await updateTaskStatus('completed'); return }
5915
6601
 
5916
6602
  if (command === 'datasets' && subcommand === 'upload') {
5917
6603
  await uploadDataset()
@@ -5927,55 +6613,60 @@ export async function main(rawArgs = process.argv.slice(2)) {
5927
6613
  await downloadDataset()
5928
6614
  return
5929
6615
  }
5930
-
5931
6616
  if (command === 'datasets' && subcommand === 'append') {
5932
6617
  await appendDatasetRows()
5933
6618
  return
5934
6619
  }
5935
-
5936
6620
  const datasetsAction = cliArgs[2]
5937
6621
  if (command === 'datasets' && subcommand === 'readme' && datasetsAction === 'set') {
5938
6622
  await setDatasetReadme()
5939
6623
  return
5940
6624
  }
5941
-
5942
6625
  if (command === 'datasets' && subcommand === 'versions' && datasetsAction === 'create') {
5943
6626
  await createDatasetVersion()
5944
6627
  return
5945
6628
  }
5946
-
5947
6629
  if (command === 'datasets' && subcommand === 'splits' && datasetsAction === 'create') {
5948
6630
  await createDatasetSplitSet()
5949
6631
  return
5950
6632
  }
5951
-
5952
6633
  if (command === 'datasets' && subcommand === 'edit-rows') {
5953
6634
  await editDatasetRows()
5954
6635
  return
5955
6636
  }
5956
-
5957
6637
  if (command === 'datasets' && subcommand === 'delete-rows') {
5958
6638
  await deleteDatasetRows()
5959
6639
  return
5960
6640
  }
5961
-
5962
6641
  if (command === 'datasets' && subcommand === 'delete') {
5963
6642
  await deleteDataset()
5964
6643
  return
5965
6644
  }
5966
-
5967
- if (command === 'datasets' && subcommand === 'lock') {
5968
- await lockDataset()
6645
+ if (command === 'datasets' && subcommand === 'lock') { await lockDataset(); return }
6646
+ if (command === 'datasets' && subcommand === 'clone') { await cloneDataset(); return }
6647
+ if (command === 'tasks' && subcommand === 'export') { await downloadAnnotations(); return }
6648
+ if (command === 'internal' || (command === 'session' && subcommand === 'start' && hasArg('--hosted'))) { process.exitCode = await hostedCommand(cliArgs, { json: hasJsonFlag(), print: printLine, printErr: printError }); return }
6649
+ if (command === 'session' || command === 'run') {
6650
+ process.exitCode = await workbenchCommand(cliArgs, { json: hasJsonFlag(), print: printLine })
5969
6651
  return
5970
6652
  }
5971
-
5972
- if (command === 'datasets' && subcommand === 'clone') {
5973
- await cloneDataset()
6653
+ if (command === 'workspace') {
6654
+ process.exitCode = await workspaceSyncCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine })
5974
6655
  return
5975
6656
  }
5976
-
5977
- if (command === 'tasks' && subcommand === 'export') {
5978
- await downloadAnnotations()
6657
+ if (command === 'git-credential') { process.exitCode = await runGitCredential(subcommand ?? 'get', { stdin: readFileSync(0, 'utf8'), cwd: process.cwd(), print: printLine, printErr: printError }); return }
6658
+ if (command === 'github' && subcommand === 'link') { const noInput = hasArg('--no-input') || hasArg('--non-interactive') || !isInteractiveTerminal(); const team = getOptionalArgValue('--team') || (await resolveSetupTeam(null, noInput, false)).slug; await runGithubLink(team, { print: printLine, fetcher: authedFetch, openUrl: openInBrowser, confirm: (org) => askYesNo(`Connect this team to GitHub org '${org}'?`, true) }); return }
6659
+ if (command === 'connectors' || command === 'manifests') {
6660
+ const moduleCommand = command === 'connectors' ? connectorsCommand : manifestsCommand
6661
+ process.exitCode = await moduleCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, resolveProjectSlug })
6662
+ return
6663
+ }
6664
+ if (command === 'team' && (subcommand === 'kill-agents' || subcommand === 'release-agents')) {
6665
+ process.exitCode = await killSwitchCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, printErr: printError })
6666
+ return
6667
+ }
6668
+ if (command === 'team' && subcommand === 'egress-allowlist') {
6669
+ process.exitCode = await egressAllowlistCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, printErr: printError })
5979
6670
  return
5980
6671
  }
5981
6672
 
@@ -5998,7 +6689,7 @@ function isCliEntrypoint(): boolean {
5998
6689
 
5999
6690
  if (isCliEntrypoint()) {
6000
6691
  main().catch(error => {
6001
- console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'))
6692
+ printError(error instanceof Error ? error.message : 'Unknown error')
6002
6693
  process.exit(1)
6003
6694
  })
6004
6695
  }