orizu 0.4.0 → 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 +276 -21
  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 +945 -258
  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 +96 -0
  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 +277 -21
  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 +1102 -317
  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 +164 -0
  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 +8 -4
  76. package/vendor/skills/orizu-cli/references/cli-reference.md +12 -4
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
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';
7
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'fs';
7
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'fs';
8
8
  import { spawn, spawnSync } from 'child_process';
9
- import { tmpdir } from 'os';
9
+ import { homedir, tmpdir } from 'os';
10
10
  import { createInterface } from 'readline/promises';
11
+ import { emitKeypressEvents } from 'readline';
11
12
  import { stdin as input, stdout as output } from 'process';
12
13
  import { clearServerCredentials, getServerCredentials, saveServerCredentials } from './credentials.js';
13
14
  import { parseDatasetFile } from './file-parser.js';
@@ -16,14 +17,45 @@ import { parseDatasetReference } from './dataset-download.js';
16
17
  import { extractErrorMessage } from './error-response.js';
17
18
  import { parseGlobalFlags } from './global-flags.js';
18
19
  import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js';
20
+ import { zipDirectoryToBase64 } from './artifact-archive.js';
19
21
  import { runLocalAppPreview } from './preview-runtime.js';
20
- import { assertSecureTokenTransport, authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags, } from './http.js';
22
+ import { readAssignmentManifestJsonlFile } from './task-assignment-manifest.js';
23
+ import { assertSecureTokenTransport, authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags } from './http.js';
21
24
  import { computeSkillContentHash, getSkillInstallPath, getSkillTargetStatus, getTargetForAgent, installSkillTarget, isSkillInstallAgent, isSkillInstallTarget, resolveSkillSource, SKILL_INSTALL_AGENTS, SKILL_INSTALL_TARGETS, targetNeedsOverwrite, } from './skill-installer.js';
22
25
  import { renderBanner } from './banner.js';
23
26
  import { formatTaskCreateError } from './task-create-error.js';
24
27
  import { renderAgentSetupPrompt } from './setup-prompt.js';
25
- import { getWorkspaceRoot, initOrizuWorkspace, WORKSPACE_DIR_NAME, workspaceExists, } from './workspace.js';
28
+ import { getWorkspaceRoot, initOrizuWorkspace, workspaceExists, } from './workspace.js';
29
+ import { connectorsCommand } from './connectors-cli.js';
30
+ import { killSwitchCommand } from './kill-switch-cli.js';
31
+ import { egressAllowlistCommand } from './egress-allowlist-cli.js';
32
+ import { manifestsCommand } from './manifests-cli.js';
33
+ import { workbenchCommand } from './workbench-cli.js';
34
+ import { hostedCommand } from './hosted-session-cli.js';
35
+ import { workspaceSyncCommand } from './workspace-sync.js';
36
+ import { runGitCredential } from './git-credential.js';
37
+ import { runGithubLink, runInteractiveHostedSetup } from './github-setup.js';
38
+ function getErrorMessage(error) {
39
+ return error instanceof Error ? error.message : String(error);
40
+ }
41
+ function isNodeError(error) {
42
+ return error instanceof Error;
43
+ }
26
44
  const MAX_README_LENGTH = 200_000;
45
+ const TASK_STATUS_ACTION_LABELS = {
46
+ paused: {
47
+ verb: 'pause',
48
+ pastTense: 'Paused',
49
+ },
50
+ active: {
51
+ verb: 'unpause',
52
+ pastTense: 'Unpaused',
53
+ },
54
+ completed: {
55
+ verb: 'complete',
56
+ pastTense: 'Completed',
57
+ },
58
+ };
27
59
  function getCliVersion() {
28
60
  const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
29
61
  if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
@@ -48,12 +80,6 @@ function printUsage() {
48
80
  printBannerIfInteractive();
49
81
  printLine(renderRootHelp());
50
82
  }
51
- function printPreviewUsage() {
52
- 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>]`);
53
- }
54
- function printOptimizationUsage() {
55
- 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]`);
56
- }
57
83
  let cliArgs = process.argv.slice(2);
58
84
  let cliJsonOutput = false;
59
85
  function getArg(name) {
@@ -78,6 +104,17 @@ function getArgs(name) {
78
104
  }
79
105
  return values;
80
106
  }
107
+ function getOptionalArgValue(name) {
108
+ const index = cliArgs.indexOf(name);
109
+ if (index === -1 || index + 1 >= cliArgs.length) {
110
+ return null;
111
+ }
112
+ const value = cliArgs[index + 1];
113
+ if (!value || value.startsWith('-')) {
114
+ return null;
115
+ }
116
+ return value;
117
+ }
81
118
  function rejectDashPrefixedOptionValue(name, value) {
82
119
  if (value && value.startsWith('-')) {
83
120
  throw new Error(`Invalid value for ${name}: option values cannot start with a dash`);
@@ -122,6 +159,9 @@ export function createCodeChallenge(verifier) {
122
159
  export function sanitizeTerminalText(value) {
123
160
  return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
124
161
  }
162
+ function printError(message) {
163
+ console.error(sanitizeTerminalText(message));
164
+ }
125
165
  export function validateBrowserUrl(url, expectedOrigin) {
126
166
  let parsed;
127
167
  try {
@@ -265,7 +305,7 @@ function readJsonObjectArg(valueArg, label) {
265
305
  return parsed;
266
306
  }
267
307
  catch (error) {
268
- throw new Error(`Invalid ${label} '${valueArg}': ${error?.message || String(error)}`);
308
+ throw new Error(`Invalid ${label} '${valueArg}': ${getErrorMessage(error)}`);
269
309
  }
270
310
  }
271
311
  function readJsonPayloadArg(pathArg) {
@@ -351,6 +391,38 @@ async function fetchTeamMembers(teamSlug) {
351
391
  const data = await parseJsonResponse(response, 'Team members list');
352
392
  return data.members;
353
393
  }
394
+ async function createTeamOnServer(name) {
395
+ const response = await authedFetch('/api/cli/teams', {
396
+ method: 'POST',
397
+ headers: { 'Content-Type': 'application/json' },
398
+ body: JSON.stringify({ name }),
399
+ });
400
+ if (!response.ok) {
401
+ throw new Error(`Failed to create team: ${await response.text()}`);
402
+ }
403
+ const data = await parseJsonResponse(response, 'Team create');
404
+ return data.team;
405
+ }
406
+ async function createProjectOnServer(teamSlug, name) {
407
+ const response = await authedFetch('/api/cli/projects', {
408
+ method: 'POST',
409
+ headers: { 'Content-Type': 'application/json' },
410
+ body: JSON.stringify({ teamSlug, name }),
411
+ });
412
+ if (!response.ok) {
413
+ throw new Error(`Failed to create project: ${await response.text()}`);
414
+ }
415
+ const data = await parseJsonResponse(response, 'Project create');
416
+ return {
417
+ id: data.project.id,
418
+ name: data.project.name,
419
+ slug: data.project.slug,
420
+ teamId: data.project.teamId || '',
421
+ teamName: data.project.teamName || data.project.teamSlug,
422
+ teamSlug: data.project.teamSlug,
423
+ role: data.project.role || 'admin',
424
+ };
425
+ }
354
426
  async function resolveProjectSelection(projectArg) {
355
427
  projectArg = projectArg || process.env.ORIZU_PROJECT || null;
356
428
  const teams = await fetchTeams();
@@ -370,7 +442,7 @@ async function resolveProjectSelection(projectArg) {
370
442
  const [teamSlug, projectSlug] = segments.map(normalizeSlugInput);
371
443
  const matchedTeam = teams.find(team => team.slug === teamSlug);
372
444
  if (!matchedTeam) {
373
- console.error(`Team '${sanitizeTerminalText(teamSlug)}' not found in your accessible teams.`);
445
+ printError(`Team '${teamSlug}' not found in your accessible teams.`);
374
446
  const selectedTeam = await promptSelect('Select a team', teams, team => `${team.name} (${team.slug})`);
375
447
  const projects = await fetchProjects(selectedTeam.slug);
376
448
  const selectedProject = await promptSelect(`Select a project in ${selectedTeam.slug}`, projects, project => `${project.name} (${project.teamSlug}/${project.slug})`);
@@ -379,7 +451,7 @@ async function resolveProjectSelection(projectArg) {
379
451
  const projects = await fetchProjects(matchedTeam.slug);
380
452
  const matchedProject = projects.find(project => project.slug === projectSlug);
381
453
  if (!matchedProject) {
382
- console.error(`Project '${sanitizeTerminalText(projectSlug)}' not found in team '${sanitizeTerminalText(matchedTeam.slug)}'.`);
454
+ printError(`Project '${projectSlug}' not found in team '${matchedTeam.slug}'.`);
383
455
  const selectedProject = await promptSelect(`Select a project in ${matchedTeam.slug}`, projects, project => `${project.name} (${project.teamSlug}/${project.slug})`);
384
456
  return selectedProject;
385
457
  }
@@ -1988,20 +2060,12 @@ async function createTeam() {
1988
2060
  if (!name) {
1989
2061
  throw new Error('Usage: orizu teams create --name <name>');
1990
2062
  }
1991
- const response = await authedFetch('/api/cli/teams', {
1992
- method: 'POST',
1993
- headers: { 'Content-Type': 'application/json' },
1994
- body: JSON.stringify({ name }),
1995
- });
1996
- if (!response.ok) {
1997
- throw new Error(`Failed to create team: ${await response.text()}`);
1998
- }
1999
- const data = await parseJsonResponse(response, 'Team create');
2063
+ const team = await createTeamOnServer(name);
2000
2064
  if (hasJsonFlag()) {
2001
- printJson({ team: data.team });
2065
+ printJson({ team });
2002
2066
  return;
2003
2067
  }
2004
- printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`);
2068
+ printLine(`Created team: ${sanitizeTerminalText(team.name)} (${sanitizeTerminalText(team.slug)})`);
2005
2069
  }
2006
2070
  async function listProjects() {
2007
2071
  const teamSlugArg = getArg('--team');
@@ -2024,20 +2088,12 @@ async function createProject() {
2024
2088
  const team = await promptSelect('Select a team', await fetchTeams(), item => `${item.name} (${item.slug})`, { forcePrompt: true });
2025
2089
  teamSlug = team.slug;
2026
2090
  }
2027
- const response = await authedFetch('/api/cli/projects', {
2028
- method: 'POST',
2029
- headers: { 'Content-Type': 'application/json' },
2030
- body: JSON.stringify({ teamSlug, name }),
2031
- });
2032
- if (!response.ok) {
2033
- throw new Error(`Failed to create project: ${await response.text()}`);
2034
- }
2035
- const data = await parseJsonResponse(response, 'Project create');
2091
+ const project = await createProjectOnServer(teamSlug, name);
2036
2092
  if (hasJsonFlag()) {
2037
- printJson({ project: data.project });
2093
+ printJson({ project });
2038
2094
  return;
2039
2095
  }
2040
- printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`);
2096
+ printLine(`Created project ${sanitizeTerminalText(`${project.teamSlug}/${project.slug}`)}`);
2041
2097
  }
2042
2098
  async function listTasks() {
2043
2099
  const project = getArg('--project');
@@ -2063,10 +2119,10 @@ function readSourceFile(pathArg) {
2063
2119
  return readFileSync(expandedPath, 'utf-8');
2064
2120
  }
2065
2121
  catch (error) {
2066
- if (error?.code === 'ENOENT') {
2122
+ if (isNodeError(error) && error.code === 'ENOENT') {
2067
2123
  throw new Error(`File not found: ${expandedPath}`);
2068
2124
  }
2069
- throw new Error(`Failed to read file '${expandedPath}': ${error?.message || String(error)}`);
2125
+ throw new Error(`Failed to read file '${expandedPath}': ${getErrorMessage(error)}`);
2070
2126
  }
2071
2127
  }
2072
2128
  function readSourceBytes(pathArg) {
@@ -2075,10 +2131,10 @@ function readSourceBytes(pathArg) {
2075
2131
  return readFileSync(expandedPath);
2076
2132
  }
2077
2133
  catch (error) {
2078
- if (error?.code === 'ENOENT') {
2134
+ if (isNodeError(error) && error.code === 'ENOENT') {
2079
2135
  throw new Error(`File not found: ${expandedPath}`);
2080
2136
  }
2081
- throw new Error(`Failed to read file '${expandedPath}': ${error?.message || String(error)}`);
2137
+ throw new Error(`Failed to read file '${expandedPath}': ${getErrorMessage(error)}`);
2082
2138
  }
2083
2139
  }
2084
2140
  function hasJsonFlag() {
@@ -2115,12 +2171,261 @@ async function askYesNo(question, defaultYes) {
2115
2171
  rl.close();
2116
2172
  }
2117
2173
  }
2174
+ async function askText(question, defaultValue) {
2175
+ const rl = createInterface({ input, output });
2176
+ try {
2177
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
2178
+ const answer = (await rl.question(`${question}${suffix} `)).trim();
2179
+ return answer || defaultValue;
2180
+ }
2181
+ finally {
2182
+ rl.close();
2183
+ }
2184
+ }
2185
+ async function waitForAnyKeyOrEscape(message) {
2186
+ if (!isInteractiveTerminal()) {
2187
+ throw new Error('Interactive terminal required.');
2188
+ }
2189
+ printLine(message);
2190
+ const rawInput = input;
2191
+ const wasRaw = Boolean(rawInput.isRaw);
2192
+ emitKeypressEvents(input);
2193
+ return new Promise((resolvePromise, rejectPromise) => {
2194
+ function cleanup() {
2195
+ input.off('keypress', onKeypress);
2196
+ if (rawInput.setRawMode && !wasRaw) {
2197
+ rawInput.setRawMode(false);
2198
+ }
2199
+ input.pause();
2200
+ printLine('');
2201
+ }
2202
+ function onKeypress(_chunk, key) {
2203
+ cleanup();
2204
+ if (key.ctrl && key.name === 'c') {
2205
+ rejectPromise(new Error('Setup cancelled.'));
2206
+ return;
2207
+ }
2208
+ resolvePromise(key.name !== 'escape');
2209
+ }
2210
+ input.on('keypress', onKeypress);
2211
+ input.resume();
2212
+ rawInput.setRawMode?.(true);
2213
+ });
2214
+ }
2215
+ async function promptKeyboardSelect(title, items, label) {
2216
+ if (items.length === 0) {
2217
+ throw new Error(`No options available for ${title.toLowerCase()}`);
2218
+ }
2219
+ if (!isInteractiveTerminal()) {
2220
+ throw new Error(`${title} selection requires interactive terminal. Provide flags explicitly instead.`);
2221
+ }
2222
+ printLine(`\n${sanitizeTerminalText(title)}`);
2223
+ printLine('Use ↑/↓ to choose, Enter to confirm, or Esc to cancel.');
2224
+ let selected = 0;
2225
+ let rendered = false;
2226
+ const rawInput = input;
2227
+ const wasRaw = Boolean(rawInput.isRaw);
2228
+ emitKeypressEvents(input);
2229
+ function render() {
2230
+ if (rendered) {
2231
+ output.write(`\x1b[${items.length}A`);
2232
+ }
2233
+ items.forEach((item, index) => {
2234
+ output.write('\x1b[2K');
2235
+ const marker = index === selected ? '●' : '○';
2236
+ output.write(` ${marker} ${sanitizeTerminalText(label(item, index))}\n`);
2237
+ });
2238
+ rendered = true;
2239
+ }
2240
+ render();
2241
+ output.write('\x1b[?25l');
2242
+ return new Promise((resolvePromise, rejectPromise) => {
2243
+ function cleanup() {
2244
+ input.off('keypress', onKeypress);
2245
+ if (rawInput.setRawMode && !wasRaw) {
2246
+ rawInput.setRawMode(false);
2247
+ }
2248
+ input.pause();
2249
+ output.write('\x1b[?25h');
2250
+ printLine('');
2251
+ }
2252
+ function onKeypress(_chunk, key) {
2253
+ if (key.ctrl && key.name === 'c') {
2254
+ cleanup();
2255
+ rejectPromise(new Error('Setup cancelled.'));
2256
+ return;
2257
+ }
2258
+ if (key.name === 'escape') {
2259
+ cleanup();
2260
+ rejectPromise(new Error('Setup cancelled.'));
2261
+ return;
2262
+ }
2263
+ if (key.name === 'up') {
2264
+ selected = (selected - 1 + items.length) % items.length;
2265
+ render();
2266
+ return;
2267
+ }
2268
+ if (key.name === 'down') {
2269
+ selected = (selected + 1) % items.length;
2270
+ render();
2271
+ return;
2272
+ }
2273
+ if (key.name === 'return' || key.name === 'enter') {
2274
+ const item = items[selected];
2275
+ cleanup();
2276
+ resolvePromise(item);
2277
+ }
2278
+ }
2279
+ input.on('keypress', onKeypress);
2280
+ input.resume();
2281
+ rawInput.setRawMode?.(true);
2282
+ });
2283
+ }
2284
+ async function promptKeyboardMultiSelect(title, items, label, defaultSelected) {
2285
+ if (items.length === 0) {
2286
+ return [];
2287
+ }
2288
+ if (!isInteractiveTerminal()) {
2289
+ throw new Error(`${title} selection requires interactive terminal. Provide flags explicitly instead.`);
2290
+ }
2291
+ printLine(`\n${sanitizeTerminalText(title)}`);
2292
+ printLine('Use ↑/↓ to move, Space to toggle, Enter to confirm, or Esc to skip.');
2293
+ let selected = 0;
2294
+ let rendered = false;
2295
+ const checked = new Set();
2296
+ items.forEach((item, index) => {
2297
+ if (defaultSelected(item, index)) {
2298
+ checked.add(index);
2299
+ }
2300
+ });
2301
+ const rawInput = input;
2302
+ const wasRaw = Boolean(rawInput.isRaw);
2303
+ emitKeypressEvents(input);
2304
+ function render() {
2305
+ if (rendered) {
2306
+ output.write(`\x1b[${items.length}A`);
2307
+ }
2308
+ items.forEach((item, index) => {
2309
+ output.write('\x1b[2K');
2310
+ const cursor = index === selected ? '›' : ' ';
2311
+ const marker = checked.has(index) ? '◉' : '○';
2312
+ output.write(`${cursor} ${marker} ${sanitizeTerminalText(label(item, index))}\n`);
2313
+ });
2314
+ rendered = true;
2315
+ }
2316
+ render();
2317
+ output.write('\x1b[?25l');
2318
+ return new Promise((resolvePromise, rejectPromise) => {
2319
+ function cleanup() {
2320
+ input.off('keypress', onKeypress);
2321
+ if (rawInput.setRawMode && !wasRaw) {
2322
+ rawInput.setRawMode(false);
2323
+ }
2324
+ input.pause();
2325
+ output.write('\x1b[?25h');
2326
+ printLine('');
2327
+ }
2328
+ function onKeypress(_chunk, key) {
2329
+ if (key.ctrl && key.name === 'c') {
2330
+ cleanup();
2331
+ rejectPromise(new Error('Setup cancelled.'));
2332
+ return;
2333
+ }
2334
+ if (key.name === 'escape') {
2335
+ cleanup();
2336
+ resolvePromise([]);
2337
+ return;
2338
+ }
2339
+ if (key.name === 'up') {
2340
+ selected = (selected - 1 + items.length) % items.length;
2341
+ render();
2342
+ return;
2343
+ }
2344
+ if (key.name === 'down') {
2345
+ selected = (selected + 1) % items.length;
2346
+ render();
2347
+ return;
2348
+ }
2349
+ if (key.name === 'space') {
2350
+ if (checked.has(selected)) {
2351
+ checked.delete(selected);
2352
+ }
2353
+ else {
2354
+ checked.add(selected);
2355
+ }
2356
+ render();
2357
+ return;
2358
+ }
2359
+ if (key.name === 'return' || key.name === 'enter') {
2360
+ const selectedItems = items.filter((_item, index) => checked.has(index));
2361
+ cleanup();
2362
+ resolvePromise(selectedItems);
2363
+ }
2364
+ }
2365
+ input.on('keypress', onKeypress);
2366
+ input.resume();
2367
+ rawInput.setRawMode?.(true);
2368
+ });
2369
+ }
2370
+ function formatFindingCount(count, label) {
2371
+ return `${count} ${label}${count === 1 ? '' : 's'}`;
2372
+ }
2373
+ function validationSummary(findings, logPath) {
2374
+ if (findings.length === 0) {
2375
+ return 'passed';
2376
+ }
2377
+ const errors = findings.filter(finding => finding.severity === 'error').length;
2378
+ const warnings = findings.filter(finding => finding.severity === 'warning').length;
2379
+ const infos = findings.filter(finding => finding.severity === 'info').length;
2380
+ const parts = [
2381
+ errors > 0 ? formatFindingCount(errors, 'error') : null,
2382
+ warnings > 0 ? formatFindingCount(warnings, 'warning') : null,
2383
+ infos > 0 ? formatFindingCount(infos, 'info') : null,
2384
+ ].filter(Boolean);
2385
+ const location = logPath ? ` (printed to ${logPath})` : '';
2386
+ return `${parts.join(', ')}${location}`;
2387
+ }
2388
+ function writeWorkspaceValidationLog(workspaceRoot, findings, dryRun) {
2389
+ if (findings.length === 0 || dryRun) {
2390
+ return null;
2391
+ }
2392
+ const logsDir = join(workspaceRoot, '.logs');
2393
+ mkdirSync(logsDir, { recursive: true });
2394
+ const logPath = join(logsDir, `${randomBytes(6).toString('hex')}.log`);
2395
+ const lines = [
2396
+ 'Orizu workspace validation',
2397
+ `Generated at: ${new Date().toISOString()}`,
2398
+ `Workspace: ${workspaceRoot}`,
2399
+ `Summary: ${validationSummary(findings, null)}`,
2400
+ '',
2401
+ ...findings.map(finding => {
2402
+ const path = finding.path ? ` ${finding.path}` : '';
2403
+ return `${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`;
2404
+ }),
2405
+ '',
2406
+ ];
2407
+ writeFileSync(logPath, lines.join('\n'), 'utf8');
2408
+ return relative(workspaceRoot, logPath);
2409
+ }
2410
+ function printWorkspaceFindings(findings) {
2411
+ if (findings.length === 0) {
2412
+ printLine(' Validation passed.');
2413
+ return;
2414
+ }
2415
+ printLine(' Validation findings:');
2416
+ for (const finding of findings) {
2417
+ const path = finding.path ? ` ${finding.path}` : '';
2418
+ printLine(` - ${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`);
2419
+ }
2420
+ }
2118
2421
  const SKILL_INSTALL_USAGE = `Usage: orizu install-skill [--agent <${SKILL_INSTALL_AGENTS.join('|')}>]... [--scope global|project] [--mode auto|link|copy] [--target <${SKILL_INSTALL_TARGETS.join('|')}>]... [--yes] [--dry-run]`;
2119
2422
  function describeSkillTarget(target) {
2120
2423
  if (target === 'claude-user')
2121
2424
  return 'Claude Code, for you across all projects';
2122
2425
  if (target === 'claude-project')
2123
2426
  return 'Claude Code, only for this project';
2427
+ if (target === 'codex-user')
2428
+ return 'Codex, for you across all projects';
2124
2429
  if (target === 'agent-user')
2125
2430
  return 'Codex / Open Agent Skills, for you across all projects';
2126
2431
  if (target === 'agents-project')
@@ -2222,8 +2527,16 @@ function describePlannedWrite(target, action, mode, path) {
2222
2527
  async function applySkillInstallTargets(targets, options) {
2223
2528
  const cliVersion = getCliVersion();
2224
2529
  const jsonMode = hasJsonFlag();
2225
- const planned = targets.map(target => installSkillTarget(target, { dryRun: true, mode: options.mode, cliVersion }));
2226
- if (!jsonMode) {
2530
+ const planned = targets.map(target => installSkillTarget(target, {
2531
+ cwd: options.cwd,
2532
+ homeDir: options.homeDir,
2533
+ dryRun: true,
2534
+ mode: options.mode,
2535
+ cliVersion,
2536
+ }));
2537
+ const showPlan = options.showPlan ?? true;
2538
+ const printResults = options.printResults ?? true;
2539
+ if (showPlan && !jsonMode) {
2227
2540
  printLine('Planned writes:');
2228
2541
  for (const plan of planned) {
2229
2542
  printLine(describePlannedWrite(plan.target, plan.action, plan.mode, plan.path));
@@ -2231,7 +2544,7 @@ async function applySkillInstallTargets(targets, options) {
2231
2544
  printLine('');
2232
2545
  }
2233
2546
  if (options.dryRun) {
2234
- if (!jsonMode) {
2547
+ if (printResults && !jsonMode) {
2235
2548
  printLine('Dry run: no files were changed.');
2236
2549
  }
2237
2550
  return planned.map(plan => ({
@@ -2244,8 +2557,8 @@ async function applySkillInstallTargets(targets, options) {
2244
2557
  const outcomes = [];
2245
2558
  for (const target of targets) {
2246
2559
  let overwrite = options.skipConfirm;
2247
- const installPath = getSkillInstallPath(target);
2248
- if (!overwrite && targetNeedsOverwrite(target)) {
2560
+ const installPath = getSkillInstallPath(target, { cwd: options.cwd, homeDir: options.homeDir });
2561
+ if (!overwrite && targetNeedsOverwrite(target, { cwd: options.cwd, homeDir: options.homeDir })) {
2249
2562
  if (!isInteractiveTerminal()) {
2250
2563
  throw new Error(`${installPath} already exists. Pass --yes to replace it.`);
2251
2564
  }
@@ -2260,11 +2573,13 @@ async function applySkillInstallTargets(targets, options) {
2260
2573
  }
2261
2574
  try {
2262
2575
  const result = installSkillTarget(target, {
2576
+ cwd: options.cwd,
2577
+ homeDir: options.homeDir,
2263
2578
  overwrite,
2264
2579
  mode: options.mode,
2265
2580
  cliVersion,
2266
2581
  });
2267
- if (!jsonMode) {
2582
+ if (printResults && !jsonMode) {
2268
2583
  const suffix = result.mode === 'link' ? ' (symlink)' : '';
2269
2584
  printLine(`${formatSkillInstallAction(result.action)} ${result.path}${suffix}`);
2270
2585
  }
@@ -2276,8 +2591,8 @@ async function applySkillInstallTargets(targets, options) {
2276
2591
  });
2277
2592
  }
2278
2593
  catch (error) {
2279
- const message = error?.message || String(error);
2280
- if (!jsonMode) {
2594
+ const message = getErrorMessage(error);
2595
+ if (printResults && !jsonMode) {
2281
2596
  printLine(`Failed ${installPath}: ${message}`);
2282
2597
  }
2283
2598
  outcomes.push({ target, path: installPath, action: 'failed', mode: null, error: message });
@@ -2439,15 +2754,124 @@ function describeSetupAuthState() {
2439
2754
  baseUrl,
2440
2755
  };
2441
2756
  }
2757
+ async function resolveSetupTeam(teamSlugArg, noInput, dryRun) {
2758
+ const teams = await fetchTeams();
2759
+ const normalizedTeamSlug = teamSlugArg ? normalizeSlugInput(teamSlugArg) : null;
2760
+ if (normalizedTeamSlug) {
2761
+ const team = teams.find(candidate => candidate.slug === normalizedTeamSlug);
2762
+ if (!team) {
2763
+ throw new Error(`Team '${normalizedTeamSlug}' was not found in your accessible teams.`);
2764
+ }
2765
+ return team;
2766
+ }
2767
+ if (noInput || dryRun) {
2768
+ throw new Error('Authenticated setup requires --team <slug> when running non-interactively.');
2769
+ }
2770
+ const choices = [
2771
+ ...teams.map(team => ({ kind: 'team', team })),
2772
+ { kind: 'create' },
2773
+ ];
2774
+ const choice = await promptKeyboardSelect('Choose the team to set up in this directory', choices, item => item.kind === 'create'
2775
+ ? 'Create a new team'
2776
+ : `${item.team.name} (${item.team.slug})`);
2777
+ if (choice.kind === 'team') {
2778
+ return choice.team;
2779
+ }
2780
+ const name = await askText(' Team name?', '');
2781
+ if (!name) {
2782
+ throw new Error('Team name is required to create a team.');
2783
+ }
2784
+ return createTeamOnServer(name);
2785
+ }
2786
+ async function resolveSetupProjects(team) {
2787
+ const projects = await fetchProjects(team.slug);
2788
+ if (projects.length === 0) {
2789
+ throw new Error(`Team '${team.slug}' has no projects. Create a project before running setup.`);
2790
+ }
2791
+ return projects;
2792
+ }
2793
+ function projectSeedsFromProjects(projects) {
2794
+ return projects.map(project => ({
2795
+ slug: project.slug,
2796
+ id: project.id,
2797
+ name: project.name,
2798
+ }));
2799
+ }
2800
+ function formatProjectSetupProgress(count) {
2801
+ return `${count} project${count === 1 ? '' : 's'} being set up in workspace...`;
2802
+ }
2803
+ function setupTargetForAgent(agent) {
2804
+ return agent === 'codex' ? 'codex-user' : 'claude-user';
2805
+ }
2806
+ function resolveSetupSkillHome() {
2807
+ return resolve(process.env.HOME || homedir());
2808
+ }
2809
+ function detectedSetupSkillChoices(homeDir) {
2810
+ const choices = [];
2811
+ if (existsSync(join(homeDir, '.codex'))) {
2812
+ choices.push({
2813
+ agent: 'codex',
2814
+ label: 'Codex',
2815
+ target: 'codex-user',
2816
+ });
2817
+ }
2818
+ if (existsSync(join(homeDir, '.claude'))) {
2819
+ choices.push({
2820
+ agent: 'claude',
2821
+ label: 'Claude Code',
2822
+ target: 'claude-user',
2823
+ });
2824
+ }
2825
+ return choices;
2826
+ }
2827
+ function setupSkillTargetsFromArgs() {
2828
+ const targets = [];
2829
+ for (const agent of getArgs('--agent')) {
2830
+ if (!isSkillInstallAgent(agent)) {
2831
+ throw new Error(`Unknown agent '${agent}'. Available agents: ${SKILL_INSTALL_AGENTS.join(', ')}`);
2832
+ }
2833
+ const target = setupTargetForAgent(agent);
2834
+ if (!targets.includes(target)) {
2835
+ targets.push(target);
2836
+ }
2837
+ }
2838
+ for (const target of getArgs('--target')) {
2839
+ if (!isSkillInstallTarget(target)) {
2840
+ throw new Error(`Unknown skill install target '${target}'. Available targets: ${SKILL_INSTALL_TARGETS.join(', ')}`);
2841
+ }
2842
+ if (!targets.includes(target)) {
2843
+ targets.push(target);
2844
+ }
2845
+ }
2846
+ return targets;
2847
+ }
2848
+ function setupSkillLabel(target) {
2849
+ if (target === 'codex-user' || target === 'codex-project' || target === 'agent-user' || target === 'agents-project') {
2850
+ return 'Codex skill';
2851
+ }
2852
+ if (target === 'claude-user' || target === 'claude-project')
2853
+ return 'Claude Code skill';
2854
+ return 'Orizu skill';
2855
+ }
2442
2856
  async function setupCommand() {
2443
2857
  const dryRun = hasArg('--dry-run');
2858
+ const validateOnly = hasArg('--validate') && !hasArg('--fix');
2859
+ const fix = hasArg('--fix');
2860
+ const verbose = hasArg('--verbose');
2861
+ const handoffRequested = hasArg('--handoff') || Boolean(getArg('--launch'));
2444
2862
  const skipConfirm = hasArg('--yes');
2445
- const noInput = hasArg('--no-input') || !isInteractiveTerminal();
2863
+ const noInput = hasArg('--no-input') || hasArg('--non-interactive') || !isInteractiveTerminal();
2864
+ const workspaceArg = getOptionalArgValue('--workspace');
2865
+ const workspaceRoot = workspaceArg ? resolve(expandHomePath(workspaceArg)) : process.cwd();
2866
+ const teamArg = getOptionalArgValue('--team');
2867
+ const projectArg = getOptionalArgValue('--project');
2868
+ const setupSkillHome = resolveSetupSkillHome();
2869
+ const installMode = getArg('--mode') ? parseSkillInstallMode() : 'link';
2446
2870
  printBannerIfInteractive();
2447
2871
  printLine('Orizu setup');
2448
2872
  printLine('');
2449
- // Phase 1: login
2450
- printLine('1. Authentication');
2873
+ // Step 1: login
2874
+ printLine('Step 1: Login');
2451
2875
  let auth = describeSetupAuthState();
2452
2876
  if (hasArg('--skip-login')) {
2453
2877
  printLine(' Skipped (--skip-login).');
@@ -2458,72 +2882,177 @@ async function setupCommand() {
2458
2882
  else if (noInput || dryRun) {
2459
2883
  printLine(' Not signed in. Run `orizu login` to authenticate.');
2460
2884
  }
2461
- else if (await askYesNo(' Sign in to Orizu now?', true)) {
2885
+ else if (await waitForAnyKeyOrEscape(' Press any key to open a browser and connect to your account, or Esc to cancel.')) {
2462
2886
  await login();
2463
2887
  auth = describeSetupAuthState();
2464
2888
  }
2465
2889
  else {
2466
- printLine(' Skipped. Run `orizu login` when ready.');
2890
+ throw new Error('Setup cancelled.');
2467
2891
  }
2468
2892
  printLine('');
2469
- // Phase 2: agent integration
2470
- printLine('2. Agent integration');
2471
- const scope = parseSkillInstallScope();
2472
- const mode = parseSkillInstallMode();
2473
- let targets = hasArg('--no-install') ? [] : parseSkillInstallTargets(scope);
2474
- if (targets.length === 0 && !hasArg('--no-install')) {
2475
- if (noInput) {
2476
- printLine(' No agents selected. Pass --agent claude and/or --agent codex, or rerun interactively.');
2893
+ // Step 2: local workspace
2894
+ printLine('Step 2: Setup your workspace');
2895
+ let workspaceState = 'skipped';
2896
+ let workspaceResult = null;
2897
+ let validationLogPath = null;
2898
+ let hostedAttached = false;
2899
+ if (hasArg('--no-workspace') && !validateOnly && !fix) {
2900
+ printLine(' Skipped (--no-workspace).');
2901
+ }
2902
+ else {
2903
+ const wantsWorkspace = validateOnly
2904
+ || fix
2905
+ || hasArg('--workspace')
2906
+ || Boolean(teamArg || projectArg)
2907
+ || workspaceExists(workspaceRoot)
2908
+ || !noInput;
2909
+ if (wantsWorkspace) {
2910
+ let teamSlug = teamArg;
2911
+ const projectSlug = projectArg;
2912
+ let teamId = null;
2913
+ let projects;
2914
+ if (auth.state === 'signed-in' && !validateOnly) {
2915
+ const team = await resolveSetupTeam(teamSlug, noInput, dryRun);
2916
+ const serverProjects = await resolveSetupProjects(team);
2917
+ teamSlug = team.slug;
2918
+ teamId = team.id;
2919
+ projects = projectSeedsFromProjects(serverProjects);
2920
+ printLine(` ${formatProjectSetupProgress(projects.length)}`);
2921
+ if (!hasArg('--local') && !noInput && !dryRun) {
2922
+ let link = null;
2923
+ try {
2924
+ link = await runGithubLink(team.slug, { print: printLine, fetcher: authedFetch, openUrl: openInBrowser, confirm: (org) => askYesNo(` Connect this team to GitHub org '${org}'?`, true) });
2925
+ }
2926
+ catch (error) {
2927
+ printLine(` GitHub link skipped: ${getErrorMessage(error)} Run \`orizu github link --team ${team.slug}\` to finish.`);
2928
+ }
2929
+ if (link) {
2930
+ try {
2931
+ hostedAttached = (await runInteractiveHostedSetup({ teamSlug: team.slug, targetDir: workspaceRoot, local: hasArg('--local'), activeInstallation: link.status === 'active' }, { print: printLine, fetcher: authedFetch })).attached;
2932
+ }
2933
+ catch (error) {
2934
+ 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).`);
2935
+ }
2936
+ }
2937
+ }
2938
+ }
2939
+ else if (!teamSlug && !noInput && !validateOnly) {
2940
+ teamSlug = await askText(' Team slug?', 'local-team');
2941
+ }
2942
+ if (auth.state !== 'signed-in' && (!teamSlug || !projectSlug) && noInput && hasArg('--workspace')) {
2943
+ printLine(' Using starter local-only team/project. Pass --team with authentication to materialize server projects.');
2944
+ }
2945
+ if (projectSlug && auth.state !== 'signed-in') {
2946
+ projects = [{ slug: projectSlug }];
2947
+ }
2948
+ if (auth.state !== 'signed-in' && !validateOnly) {
2949
+ const localProjectCount = projects?.length || 1;
2950
+ printLine(` ${formatProjectSetupProgress(localProjectCount)}`);
2951
+ }
2952
+ if (hostedAttached) {
2953
+ workspaceState = 'exists';
2954
+ }
2955
+ else {
2956
+ const result = initOrizuWorkspace({
2957
+ workspaceRoot,
2958
+ teamSlug,
2959
+ teamId,
2960
+ projectSlug: auth.state === 'signed-in' ? null : projectSlug,
2961
+ projects,
2962
+ baseUrl: auth.baseUrl,
2963
+ serviceOrigin: getOptionalArgValue('--service-origin') || auth.baseUrl,
2964
+ attachWorkspaceId: getOptionalArgValue('--attach-workspace'),
2965
+ cliVersion: getCliVersion(),
2966
+ dryRun,
2967
+ validateOnly,
2968
+ fix,
2969
+ noSymlinks: hasArg('--no-symlinks'),
2970
+ });
2971
+ workspaceResult = result;
2972
+ workspaceState = result.state;
2973
+ if (dryRun || verbose) {
2974
+ for (const action of result.actions) {
2975
+ printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`);
2976
+ }
2977
+ }
2978
+ else if (result.actions.length > 0) {
2979
+ printLine(validateOnly ? ' Workspace validation complete' : ' Workspace setup complete');
2980
+ }
2981
+ else if (!validateOnly) {
2982
+ printLine(' Workspace setup complete');
2983
+ }
2984
+ validationLogPath = writeWorkspaceValidationLog(result.root, result.findings, dryRun);
2985
+ if (verbose) {
2986
+ printWorkspaceFindings(result.findings);
2987
+ }
2988
+ if ((validateOnly || fix) && result.findings.some(finding => finding.severity === 'error')) {
2989
+ process.exitCode = 1;
2990
+ }
2991
+ }
2477
2992
  }
2478
2993
  else {
2479
- targets = await promptSkillInstallTargets(scope);
2994
+ printLine(` Skipped. Rerun with --workspace to initialize the workspace contract.`);
2480
2995
  }
2481
2996
  }
2482
- let installOutcomes = [];
2483
- if (targets.length > 0) {
2484
- installOutcomes = await applySkillInstallTargets(targets, { mode, skipConfirm, dryRun });
2485
- }
2486
- else if (hasArg('--no-install')) {
2487
- printLine(' Skipped (--no-install).');
2488
- }
2489
2997
  printLine('');
2490
- // Phase 3: local workspace
2491
- printLine('3. Local workspace');
2492
- let workspaceState = 'skipped';
2493
- if (hasArg('--no-workspace')) {
2494
- printLine(' Skipped (--no-workspace).');
2495
- }
2496
- else if (workspaceExists()) {
2497
- workspaceState = 'exists';
2498
- printLine(` ${WORKSPACE_DIR_NAME}/ already initialized.`);
2998
+ // Step 3: global coding-agent skills
2999
+ let installOutcomes = [];
3000
+ printLine('Step 3: Install coding agent skills');
3001
+ if (hasArg('--no-install') || validateOnly || fix) {
3002
+ printLine(hasArg('--no-install') ? ' Skipped (--no-install).' : ' Skipped for validation/repair.');
2499
3003
  }
2500
3004
  else {
2501
- const wantsWorkspace = hasArg('--workspace') ||
2502
- (!noInput && await askYesNo(` Create a gitignored ${WORKSPACE_DIR_NAME}/ workspace for local Orizu artifacts?`, true));
2503
- if (wantsWorkspace) {
2504
- const result = initOrizuWorkspace({
2505
- baseUrl: auth.baseUrl,
2506
- cliVersion: getCliVersion(),
2507
- dryRun,
2508
- });
2509
- workspaceState = result.state === 'would-create' ? 'would-create' : result.state;
2510
- for (const action of result.actions) {
2511
- printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`);
3005
+ let targets = setupSkillTargetsFromArgs();
3006
+ if (targets.length === 0 && !noInput && !dryRun) {
3007
+ const choices = detectedSetupSkillChoices(setupSkillHome);
3008
+ if (choices.length > 0) {
3009
+ const selectedChoices = await promptKeyboardMultiSelect('Choose the coding agents that should learn how to use Orizu', choices, choice => choice.label, () => true);
3010
+ targets = selectedChoices.map(choice => choice.target);
2512
3011
  }
3012
+ else {
3013
+ printLine(' No Codex or Claude Code agent folders were detected.');
3014
+ }
3015
+ }
3016
+ if (targets.length === 0) {
3017
+ printLine(' Skipped skill install.');
2513
3018
  }
2514
3019
  else {
2515
- printLine(` Skipped. Rerun with --workspace to create ${WORKSPACE_DIR_NAME}/.`);
3020
+ printLine(' Installing skills...');
3021
+ installOutcomes = await applySkillInstallTargets(targets, {
3022
+ mode: installMode,
3023
+ skipConfirm,
3024
+ dryRun,
3025
+ cwd: workspaceRoot,
3026
+ homeDir: setupSkillHome,
3027
+ showPlan: false,
3028
+ printResults: false,
3029
+ });
3030
+ if (dryRun) {
3031
+ printLine(' Dry run: no skill files were changed.');
3032
+ }
3033
+ for (const outcome of installOutcomes) {
3034
+ const label = setupSkillLabel(outcome.target);
3035
+ if (outcome.action === 'failed') {
3036
+ printLine(` ${label} failed: ${outcome.error}`);
3037
+ }
3038
+ else if (outcome.action === 'skipped') {
3039
+ printLine(` ${label} skipped`);
3040
+ }
3041
+ else {
3042
+ printLine(` ${label} installed`);
3043
+ }
3044
+ }
2516
3045
  }
2517
3046
  }
2518
3047
  printLine('');
2519
3048
  // Phase 4: coding-agent handoff
2520
3049
  const handoffPrompt = renderAgentSetupPrompt({
2521
- workspacePath: workspaceState === 'created' || workspaceState === 'exists'
2522
- ? getWorkspaceRoot()
3050
+ workspacePath: workspaceResult && workspaceState !== 'invalid'
3051
+ ? getWorkspaceRoot(workspaceRoot)
2523
3052
  : null,
2524
3053
  });
2525
- if (!hasArg('--no-handoff')) {
2526
- printLine('4. Coding-agent handoff');
3054
+ if (handoffRequested && !hasArg('--no-handoff')) {
3055
+ printLine('Step 4: Coding-agent handoff');
2527
3056
  printLine(' Give your coding agent this prompt to plan repo-specific Orizu adoption:');
2528
3057
  printLine('');
2529
3058
  printLine('--- prompt start ---');
@@ -2563,7 +3092,7 @@ async function setupCommand() {
2563
3092
  const summaryTargets = installOutcomes.length > 0
2564
3093
  ? installOutcomes
2565
3094
  .filter(outcome => outcome.action !== 'failed')
2566
- .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target) }))
3095
+ .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target, { cwd: workspaceRoot, homeDir: setupSkillHome }) }))
2567
3096
  : [];
2568
3097
  const source = resolveSkillSource();
2569
3098
  if (hasJsonFlag()) {
@@ -2582,29 +3111,45 @@ async function setupCommand() {
2582
3111
  cliVersion: getCliVersion(),
2583
3112
  skillHash: computeSkillContentHash(source.root),
2584
3113
  },
2585
- workspace: { state: workspaceState, path: getWorkspaceRoot() },
3114
+ workspace: {
3115
+ state: workspaceState,
3116
+ path: getWorkspaceRoot(workspaceRoot),
3117
+ validationLogPath,
3118
+ operations: workspaceResult?.operations.map(op => ({
3119
+ action: op.action,
3120
+ path: op.path,
3121
+ safe: op.safe,
3122
+ reason: op.reason,
3123
+ entries: op.entries,
3124
+ sourcePath: op.sourcePath,
3125
+ target: op.target,
3126
+ })) || [],
3127
+ findings: workspaceResult?.findings || [],
3128
+ },
2586
3129
  });
2587
3130
  return;
2588
3131
  }
2589
3132
  printLine('Setup summary');
2590
3133
  printLine(` Auth: ${auth.state === 'signed-in' ? `signed in (${sanitizeTerminalText(auth.baseUrl)})` : 'not signed in — run `orizu login`'}`);
2591
3134
  if (summaryTargets.length > 0) {
2592
- printLine(' Integrations:');
3135
+ printLine(' Skills:');
2593
3136
  for (const { outcome, status } of summaryTargets) {
2594
3137
  const modeLabel = status.mode ? `, ${status.mode}` : '';
2595
3138
  printLine(` ${outcome.target.padEnd(16)} ${status.state}${modeLabel} ${outcome.path}`);
2596
3139
  }
2597
3140
  }
2598
3141
  else {
2599
- printLine(' Integrations: none installed this run — `orizu install-skill` sets them up.');
3142
+ printLine(' Skills: none installed this run');
2600
3143
  }
2601
3144
  const failed = installOutcomes.filter(outcome => outcome.action === 'failed');
2602
3145
  for (const failure of failed) {
2603
3146
  printLine(` ${failure.target.padEnd(16)} failed: ${failure.error}`);
2604
3147
  }
2605
- printLine(` Skill source: ${source.root} (${source.source})`);
2606
- printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : `${getWorkspaceRoot()} (${workspaceState})`}`);
2607
- printLine(' Next: hand the prompt above to your coding agent to plan repo-specific Orizu adoption.');
3148
+ printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : getWorkspaceRoot(workspaceRoot)}`);
3149
+ if (workspaceResult) {
3150
+ printLine(` Validation: ${validationSummary(workspaceResult.findings, validationLogPath)}`);
3151
+ }
3152
+ printLine(' Next: run `orizu setup prompt` if you want a coding-agent handoff prompt.');
2608
3153
  }
2609
3154
  function setupPromptCommand() {
2610
3155
  const prompt = renderAgentSetupPrompt({
@@ -2626,7 +3171,7 @@ function readJsonFile(pathArg) {
2626
3171
  return parsed;
2627
3172
  }
2628
3173
  catch (error) {
2629
- throw new Error(`Invalid JSON file '${pathArg}': ${error?.message || String(error)}`);
3174
+ throw new Error(`Invalid JSON file '${pathArg}': ${getErrorMessage(error)}`);
2630
3175
  }
2631
3176
  }
2632
3177
  function isRecord(value) {
@@ -2690,126 +3235,6 @@ function writeTextFileEnsuringDir(pathArg, content) {
2690
3235
  mkdirSync(dirname(pathArg), { recursive: true });
2691
3236
  writeFileSync(pathArg, content);
2692
3237
  }
2693
- function shouldExcludeArtifactPath(relativePath) {
2694
- const parts = relativePath.split('/');
2695
- return parts.some(part => part === '.git' ||
2696
- part === '.DS_Store' ||
2697
- part === '__pycache__' ||
2698
- part === '.pytest_cache');
2699
- }
2700
- function collectArtifactFiles(sourceDir, relativeDir = '') {
2701
- const absoluteDir = relativeDir ? join(sourceDir, relativeDir) : sourceDir;
2702
- return readdirSync(absoluteDir, { withFileTypes: true })
2703
- .flatMap(entry => {
2704
- const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
2705
- if (shouldExcludeArtifactPath(relativePath)) {
2706
- return [];
2707
- }
2708
- if (entry.isDirectory()) {
2709
- return collectArtifactFiles(sourceDir, relativePath);
2710
- }
2711
- return entry.isFile() ? [relativePath] : [];
2712
- })
2713
- .sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
2714
- }
2715
- const ZIP_GENERAL_PURPOSE_UTF8 = 0x0800;
2716
- const ZIP_DOS_TIME_2000_01_01 = 0;
2717
- const ZIP_DOS_DATE_2000_01_01 = ((2000 - 1980) << 9) | (1 << 5) | 1;
2718
- const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
2719
- let value = index;
2720
- for (let bit = 0; bit < 8; bit++) {
2721
- value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1);
2722
- }
2723
- return value >>> 0;
2724
- });
2725
- function crc32(bytes) {
2726
- let crc = 0xffffffff;
2727
- for (const byte of bytes) {
2728
- crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
2729
- }
2730
- return (crc ^ 0xffffffff) >>> 0;
2731
- }
2732
- function createStoredZip(entries) {
2733
- const localParts = [];
2734
- const centralParts = [];
2735
- let offset = 0;
2736
- for (const entry of entries) {
2737
- const name = Buffer.from(entry.path, 'utf8');
2738
- const crc = crc32(entry.data);
2739
- const localHeader = Buffer.alloc(30);
2740
- localHeader.writeUInt32LE(0x04034b50, 0);
2741
- localHeader.writeUInt16LE(10, 4);
2742
- localHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 6);
2743
- localHeader.writeUInt16LE(0, 8);
2744
- localHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 10);
2745
- localHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 12);
2746
- localHeader.writeUInt32LE(crc, 14);
2747
- localHeader.writeUInt32LE(entry.data.length, 18);
2748
- localHeader.writeUInt32LE(entry.data.length, 22);
2749
- localHeader.writeUInt16LE(name.length, 26);
2750
- localHeader.writeUInt16LE(0, 28);
2751
- localParts.push(localHeader, name, entry.data);
2752
- const centralHeader = Buffer.alloc(46);
2753
- centralHeader.writeUInt32LE(0x02014b50, 0);
2754
- centralHeader.writeUInt16LE(20, 4);
2755
- centralHeader.writeUInt16LE(10, 6);
2756
- centralHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 8);
2757
- centralHeader.writeUInt16LE(0, 10);
2758
- centralHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 12);
2759
- centralHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 14);
2760
- centralHeader.writeUInt32LE(crc, 16);
2761
- centralHeader.writeUInt32LE(entry.data.length, 20);
2762
- centralHeader.writeUInt32LE(entry.data.length, 24);
2763
- centralHeader.writeUInt16LE(name.length, 28);
2764
- centralHeader.writeUInt16LE(0, 30);
2765
- centralHeader.writeUInt16LE(0, 32);
2766
- centralHeader.writeUInt16LE(0, 34);
2767
- centralHeader.writeUInt16LE(0, 36);
2768
- centralHeader.writeUInt32LE(0, 38);
2769
- centralHeader.writeUInt32LE(offset, 42);
2770
- centralParts.push(centralHeader, name);
2771
- offset += localHeader.length + name.length + entry.data.length;
2772
- }
2773
- const centralDirectoryOffset = offset;
2774
- const centralDirectory = Buffer.concat(centralParts);
2775
- const endOfCentralDirectory = Buffer.alloc(22);
2776
- endOfCentralDirectory.writeUInt32LE(0x06054b50, 0);
2777
- endOfCentralDirectory.writeUInt16LE(0, 4);
2778
- endOfCentralDirectory.writeUInt16LE(0, 6);
2779
- endOfCentralDirectory.writeUInt16LE(entries.length, 8);
2780
- endOfCentralDirectory.writeUInt16LE(entries.length, 10);
2781
- endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12);
2782
- endOfCentralDirectory.writeUInt32LE(centralDirectoryOffset, 16);
2783
- endOfCentralDirectory.writeUInt16LE(0, 20);
2784
- return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory]);
2785
- }
2786
- function zipDirectoryToBase64(dirArg) {
2787
- const sourceDir = expandHomePath(dirArg);
2788
- try {
2789
- const stats = statSync(sourceDir);
2790
- if (!stats.isDirectory()) {
2791
- throw new Error(`${sourceDir} is not a directory`);
2792
- }
2793
- }
2794
- catch (error) {
2795
- if (error?.code === 'ENOENT') {
2796
- throw new Error(`Directory not found: ${sourceDir}`);
2797
- }
2798
- throw error;
2799
- }
2800
- const files = collectArtifactFiles(sourceDir);
2801
- if (files.length === 0) {
2802
- throw new Error(`Directory contains no artifact files: ${sourceDir}`);
2803
- }
2804
- const bytes = createStoredZip(files.map(relativePath => ({
2805
- path: relativePath,
2806
- data: readFileSync(join(sourceDir, relativePath)),
2807
- })));
2808
- return {
2809
- zipBase64: bytes.toString('base64'),
2810
- contentSha256: createHash('sha256').update(bytes).digest('hex'),
2811
- };
2812
- }
2813
3238
  function readManifestFile(dirArg) {
2814
3239
  return readJsonFile(join(expandHomePath(dirArg), 'manifest.json'));
2815
3240
  }
@@ -2986,39 +3411,89 @@ function parseCommaSeparated(value) {
2986
3411
  .map(item => item.trim())
2987
3412
  .filter(Boolean);
2988
3413
  }
3414
+ function readAssignmentManifest(path) {
3415
+ if (!path) {
3416
+ return null;
3417
+ }
3418
+ return readAssignmentManifestJsonlFile(expandHomePath(path));
3419
+ }
3420
+ function readOptionalTaskTextArg(valueFlag, fileFlag) {
3421
+ const value = getArg(valueFlag);
3422
+ const file = getArg(fileFlag);
3423
+ rejectDashPrefixedOptionValue(valueFlag, value);
3424
+ rejectDashPrefixedOptionValue(fileFlag, file);
3425
+ if (value !== null && file !== null) {
3426
+ throw new Error(`Use either ${valueFlag} or ${fileFlag}, not both.`);
3427
+ }
3428
+ if (file !== null) {
3429
+ return readSourceFile(file);
3430
+ }
3431
+ return value ?? undefined;
3432
+ }
3433
+ function readPositiveIntegerFlag(name) {
3434
+ const value = getArg(name);
3435
+ rejectDashPrefixedOptionValue(name, value);
3436
+ if (value === null) {
3437
+ return undefined;
3438
+ }
3439
+ const parsed = Number(value);
3440
+ if (!Number.isInteger(parsed) || parsed < 1) {
3441
+ throw new Error(`${name} must be a positive integer`);
3442
+ }
3443
+ return parsed;
3444
+ }
2989
3445
  async function createTask() {
2990
3446
  const projectSlug = getArg('--project');
2991
3447
  const datasetId = getArg('--dataset');
2992
3448
  const appId = getArg('--app');
2993
3449
  const title = getArg('--title');
2994
3450
  const assignees = parseCommaSeparated(getArg('--assignees'));
3451
+ const assignmentFile = getArg('--assignment-file');
2995
3452
  const publish = hasArg('--publish');
2996
3453
  const versionArg = getArg('--version');
2997
3454
  const instructions = getArg('--instructions');
2998
3455
  const labelsPerItemArg = getArg('--labels-per-item');
2999
- const labelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1;
3456
+ const parsedLabelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1;
3457
+ const labelsPerItem = Number.isInteger(parsedLabelsPerItem) && parsedLabelsPerItem > 0
3458
+ ? parsedLabelsPerItem
3459
+ : null;
3000
3460
  const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN;
3001
3461
  const versionNum = Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null;
3002
- if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0)) {
3003
- throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]');
3462
+ if (assignmentFile && assignees.length > 0) {
3463
+ throw new Error('Use either --assignment-file or --assignees, not both.');
3464
+ }
3465
+ const explicitAssignments = readAssignmentManifest(assignmentFile);
3466
+ if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0 && !explicitAssignments)) {
3467
+ throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]');
3004
3468
  }
3005
3469
  if (versionArg && versionNum === null) {
3006
3470
  throw new Error('--version must be a positive integer');
3007
3471
  }
3472
+ if (labelsPerItemArg && labelsPerItem === null) {
3473
+ throw new Error('--labels-per-item must be a positive integer');
3474
+ }
3475
+ const requestBody = {
3476
+ projectSlug,
3477
+ datasetId,
3478
+ appId,
3479
+ versionNum,
3480
+ title,
3481
+ publish,
3482
+ instructions,
3483
+ };
3484
+ if (explicitAssignments) {
3485
+ requestBody.explicitAssignments = explicitAssignments;
3486
+ }
3487
+ else {
3488
+ requestBody.memberIds = assignees;
3489
+ }
3490
+ if (!explicitAssignments || labelsPerItemArg) {
3491
+ requestBody.requiredAssignmentsPerRow = labelsPerItem;
3492
+ }
3008
3493
  const response = await authedFetch('/api/cli/tasks', {
3009
3494
  method: 'POST',
3010
3495
  headers: { 'Content-Type': 'application/json' },
3011
- body: JSON.stringify({
3012
- projectSlug,
3013
- datasetId,
3014
- appId,
3015
- versionNum,
3016
- title,
3017
- memberIds: assignees,
3018
- publish,
3019
- instructions,
3020
- requiredAssignmentsPerRow: labelsPerItem,
3021
- }),
3496
+ body: JSON.stringify(requestBody),
3022
3497
  });
3023
3498
  if (!response.ok) {
3024
3499
  const cliError = await formatTaskCreateError(response);
@@ -3041,13 +3516,16 @@ async function createTask() {
3041
3516
  taskUrl: `${getBaseUrl()}/d/${projectSlug}/tasks/${data.task.id}`,
3042
3517
  title: data.task.title,
3043
3518
  status: data.task.status,
3519
+ assignmentMode: explicitAssignments ? 'custom' : 'auto',
3044
3520
  requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
3045
3521
  assignmentsCreated: data.assignmentsCreated,
3046
3522
  draft: data.task.status === 'draft',
3047
3523
  ...(data.task.status === 'draft'
3048
3524
  ? {
3049
3525
  message: 'Draft created. Test it manually before assigning.',
3050
- publishCommand: `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
3526
+ publishCommand: explicitAssignments
3527
+ ? `orizu tasks publish --task ${data.task.id} --assignment-file ${assignmentFile ? shellQuote(assignmentFile) : '<path>'}`
3528
+ : `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
3051
3529
  }
3052
3530
  : {}),
3053
3531
  ...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
@@ -3062,24 +3540,155 @@ async function createTask() {
3062
3540
  `\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
3063
3541
  `\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
3064
3542
  `\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
3543
+ `\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
3065
3544
  `\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
3066
3545
  `\n Assignments: ${data.assignmentsCreated}` +
3067
3546
  (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
3068
3547
  `\n URL: ${sanitizeTerminalText(taskUrl)}` +
3069
3548
  (isDraft
3070
- ? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval: orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`
3549
+ ? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval: ${explicitAssignments
3550
+ ? `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignment-file ${sanitizeTerminalText(assignmentFile || '<path>')}`
3551
+ : `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`}`
3071
3552
  : ''));
3072
3553
  }
3554
+ async function updateTask() {
3555
+ const taskId = getArg('--task');
3556
+ rejectDashPrefixedOptionValue('--task', taskId);
3557
+ if (!taskId) {
3558
+ 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]');
3559
+ }
3560
+ const title = getArg('--title');
3561
+ const description = readOptionalTaskTextArg('--description', '--description-file');
3562
+ const instructions = readOptionalTaskTextArg('--instructions', '--instructions-file');
3563
+ const datasetId = getArg('--dataset');
3564
+ const appId = getArg('--app');
3565
+ const versionNum = readPositiveIntegerFlag('--version');
3566
+ const requiredAssignmentsPerRow = readPositiveIntegerFlag('--labels-per-item');
3567
+ const assignees = parseCommaSeparated(getArg('--assignees'));
3568
+ const assignmentFile = getArg('--assignment-file');
3569
+ rejectDashPrefixedOptionValue('--title', title);
3570
+ rejectDashPrefixedOptionValue('--dataset', datasetId);
3571
+ rejectDashPrefixedOptionValue('--app', appId);
3572
+ rejectDashPrefixedOptionValue('--assignment-file', assignmentFile);
3573
+ if (title !== null && title.trim().length === 0) {
3574
+ throw new Error('--title must be a non-empty string');
3575
+ }
3576
+ if (assignmentFile && assignees.length > 0) {
3577
+ throw new Error('Use either --assignment-file or --assignees, not both.');
3578
+ }
3579
+ if (hasArg('--assignees') && assignees.length === 0) {
3580
+ throw new Error('--assignees requires at least one user ID or email');
3581
+ }
3582
+ if (versionNum !== undefined && !appId) {
3583
+ throw new Error('--version requires --app');
3584
+ }
3585
+ const explicitAssignments = readAssignmentManifest(assignmentFile);
3586
+ const requestBody = {};
3587
+ if (title !== null)
3588
+ requestBody.title = title.trim();
3589
+ if (description !== undefined)
3590
+ requestBody.description = description;
3591
+ if (instructions !== undefined)
3592
+ requestBody.instructions = instructions;
3593
+ if (datasetId !== null)
3594
+ requestBody.datasetId = datasetId;
3595
+ if (appId !== null)
3596
+ requestBody.appId = appId;
3597
+ if (versionNum !== undefined)
3598
+ requestBody.versionNum = versionNum;
3599
+ if (requiredAssignmentsPerRow !== undefined) {
3600
+ requestBody.requiredAssignmentsPerRow = requiredAssignmentsPerRow;
3601
+ }
3602
+ if (explicitAssignments) {
3603
+ requestBody.explicitAssignments = explicitAssignments;
3604
+ }
3605
+ else if (hasArg('--assignees')) {
3606
+ requestBody.memberIds = assignees;
3607
+ }
3608
+ if (Object.keys(requestBody).length === 0) {
3609
+ 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]');
3610
+ }
3611
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}`, {
3612
+ method: 'PATCH',
3613
+ headers: { 'Content-Type': 'application/json' },
3614
+ body: JSON.stringify(requestBody),
3615
+ });
3616
+ if (!response.ok) {
3617
+ throw new Error(`Failed to update task: ${await extractErrorMessage(response)}`);
3618
+ }
3619
+ const data = await parseJsonResponse(response, 'Task update');
3620
+ if (hasJsonFlag()) {
3621
+ printJson({
3622
+ taskId: data.task.id,
3623
+ title: data.task.title ?? null,
3624
+ status: data.task.status ?? null,
3625
+ datasetId: data.task.dataset_id ?? null,
3626
+ appId: data.task.app_id ?? null,
3627
+ versionId: data.task.version_id ?? null,
3628
+ requiredAssignmentsPerRow: data.task.required_assignments_per_row ?? null,
3629
+ ...(data.assignmentMode ? { assignmentMode: data.assignmentMode } : {}),
3630
+ ...(data.assignmentsCreated !== undefined
3631
+ ? { assignmentsCreated: data.assignmentsCreated }
3632
+ : {}),
3633
+ });
3634
+ return;
3635
+ }
3636
+ printLine(`Updated draft task ${sanitizeTerminalText(data.task.title || data.task.id)} (${sanitizeTerminalText(data.task.id)})` +
3637
+ (data.task.status ? ` [${sanitizeTerminalText(data.task.status)}]` : '') +
3638
+ (data.task.dataset_id ? `\n Dataset ID: ${sanitizeTerminalText(data.task.dataset_id)}` : '') +
3639
+ (data.task.version_id ? `\n Version ID: ${sanitizeTerminalText(data.task.version_id)}` : '') +
3640
+ (data.task.required_assignments_per_row
3641
+ ? `\n Labels/row: ${data.task.required_assignments_per_row}`
3642
+ : '') +
3643
+ (data.assignmentMode ? `\n Mode: ${sanitizeTerminalText(data.assignmentMode === 'custom' ? 'custom row map' : 'auto distribute')}` : '') +
3644
+ (data.assignmentsCreated !== undefined ? `\n Assignments: ${data.assignmentsCreated}` : ''));
3645
+ }
3646
+ async function discardTask() {
3647
+ const taskId = getArg('--task');
3648
+ rejectDashPrefixedOptionValue('--task', taskId);
3649
+ if (!taskId) {
3650
+ throw new Error('Usage: orizu tasks discard --task <taskId> [--yes] [--json]');
3651
+ }
3652
+ if (!hasArg('--yes')) {
3653
+ if (!isInteractiveTerminal()) {
3654
+ throw new Error('Refusing to discard a draft in non-interactive mode without --yes');
3655
+ }
3656
+ const confirmed = await askYesNo(`Discard draft task ${taskId}? This cannot be undone.`, false);
3657
+ if (!confirmed) {
3658
+ printLine('Discard cancelled.');
3659
+ return;
3660
+ }
3661
+ }
3662
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}`, {
3663
+ method: 'DELETE',
3664
+ });
3665
+ if (!response.ok) {
3666
+ throw new Error(`Failed to discard draft task: ${await extractErrorMessage(response)}`);
3667
+ }
3668
+ const data = await parseJsonResponse(response, 'Task discard');
3669
+ if (hasJsonFlag()) {
3670
+ printJson({ taskId, discarded: data.success });
3671
+ return;
3672
+ }
3673
+ printLine(`Discarded draft task ${sanitizeTerminalText(taskId)}.`);
3674
+ }
3073
3675
  async function publishTask() {
3074
3676
  const taskId = getArg('--task');
3075
3677
  const assignees = parseCommaSeparated(getArg('--assignees'));
3076
- if (!taskId || assignees.length === 0) {
3077
- throw new Error('Usage: orizu tasks publish --task <taskId> --assignees <userId1,userId2> [--json]');
3678
+ const assignmentFile = getArg('--assignment-file');
3679
+ if (assignmentFile && assignees.length > 0) {
3680
+ throw new Error('Use either --assignment-file or --assignees, not both.');
3681
+ }
3682
+ const explicitAssignments = readAssignmentManifest(assignmentFile);
3683
+ if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
3684
+ throw new Error('Usage: orizu tasks publish --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--json]');
3078
3685
  }
3079
3686
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/publish`, {
3080
3687
  method: 'POST',
3081
3688
  headers: { 'Content-Type': 'application/json' },
3082
- body: JSON.stringify({ memberIds: assignees }),
3689
+ body: JSON.stringify(explicitAssignments
3690
+ ? { explicitAssignments }
3691
+ : { memberIds: assignees }),
3083
3692
  });
3084
3693
  if (!response.ok) {
3085
3694
  throw new Error(`Failed to publish task: ${await extractErrorMessage(response)}`);
@@ -3089,6 +3698,7 @@ async function publishTask() {
3089
3698
  printLine(JSON.stringify({
3090
3699
  taskId: data.task.id,
3091
3700
  status: data.task.status,
3701
+ assignmentMode: explicitAssignments ? 'custom' : 'auto',
3092
3702
  assignmentsCreated: data.assignmentsCreated,
3093
3703
  assignmentsProcessed: data.assignmentsProcessed,
3094
3704
  ...(data.assignmentsRemoved !== undefined
@@ -3102,6 +3712,7 @@ async function publishTask() {
3102
3712
  return;
3103
3713
  }
3104
3714
  printLine(`Published task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]` +
3715
+ `\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
3105
3716
  `\n Assignments: ${data.assignmentsCreated}` +
3106
3717
  (data.assignmentsRemoved !== undefined
3107
3718
  ? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
@@ -3111,23 +3722,50 @@ async function publishTask() {
3111
3722
  async function assignTask() {
3112
3723
  const taskId = getArg('--task');
3113
3724
  const assignees = parseCommaSeparated(getArg('--assignees'));
3114
- if (!taskId || assignees.length === 0) {
3115
- throw new Error('Usage: orizu tasks assign --task <taskId> --assignees <userId1,userId2>');
3725
+ const assignmentFile = getArg('--assignment-file');
3726
+ if (assignmentFile && assignees.length > 0) {
3727
+ throw new Error('Use either --assignment-file or --assignees, not both.');
3728
+ }
3729
+ const explicitAssignments = readAssignmentManifest(assignmentFile);
3730
+ if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
3731
+ throw new Error('Usage: orizu tasks assign --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--replace-existing] [--json]');
3732
+ }
3733
+ const requestBody = explicitAssignments
3734
+ ? { explicitAssignments }
3735
+ : { memberIds: assignees };
3736
+ if (hasArg('--replace-existing')) {
3737
+ requestBody.replaceExisting = true;
3116
3738
  }
3117
3739
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/assign`, {
3118
3740
  method: 'POST',
3119
3741
  headers: { 'Content-Type': 'application/json' },
3120
- body: JSON.stringify({ memberIds: assignees }),
3742
+ body: JSON.stringify(requestBody),
3121
3743
  });
3122
3744
  if (!response.ok) {
3123
3745
  throw new Error(`Failed to assign task: ${await response.text()}`);
3124
3746
  }
3125
3747
  const data = await parseJsonResponse(response, 'Task assign');
3126
3748
  if (hasJsonFlag()) {
3127
- printJson({ assignmentsCreated: data.assignmentsCreated });
3749
+ printJson({
3750
+ assignmentMode: explicitAssignments ? 'custom' : 'auto',
3751
+ assignmentsProcessed: data.assignmentsProcessed ?? data.assignmentsCreated,
3752
+ assignmentsCreated: data.assignmentsCreated,
3753
+ ...(data.assignmentsRemoved !== undefined
3754
+ ? { assignmentsRemoved: data.assignmentsRemoved }
3755
+ : {}),
3756
+ ...(data.assignmentShortfall !== undefined
3757
+ ? { assignmentShortfall: data.assignmentShortfall }
3758
+ : {}),
3759
+ ...(data.warning ? { warning: data.warning } : {}),
3760
+ });
3128
3761
  return;
3129
3762
  }
3130
- printLine(`Created ${data.assignmentsCreated} assignments.`);
3763
+ printLine(`Created ${data.assignmentsCreated} assignments.` +
3764
+ `\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
3765
+ (data.assignmentsRemoved !== undefined
3766
+ ? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
3767
+ : '') +
3768
+ (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : ''));
3131
3769
  }
3132
3770
  async function taskStatus() {
3133
3771
  const taskId = getArg('--task');
@@ -3172,9 +3810,9 @@ async function taskStatus() {
3172
3810
  }
3173
3811
  async function updateTaskStatus(targetStatus) {
3174
3812
  const taskId = getArg('--task');
3813
+ const labels = TASK_STATUS_ACTION_LABELS[targetStatus];
3175
3814
  if (!taskId) {
3176
- const verb = targetStatus === 'paused' ? 'pause' : 'unpause';
3177
- throw new Error(`Usage: orizu tasks ${verb} --task <taskId>`);
3815
+ throw new Error(`Usage: orizu tasks ${labels.verb} --task <taskId>`);
3178
3816
  }
3179
3817
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`, {
3180
3818
  method: 'PATCH',
@@ -3182,16 +3820,14 @@ async function updateTaskStatus(targetStatus) {
3182
3820
  body: JSON.stringify({ status: targetStatus }),
3183
3821
  });
3184
3822
  if (!response.ok) {
3185
- const verb = targetStatus === 'paused' ? 'pause' : 'unpause';
3186
- throw new Error(`Failed to ${verb} task: ${await response.text()}`);
3823
+ throw new Error(`Failed to ${labels.verb} task: ${await response.text()}`);
3187
3824
  }
3188
3825
  const data = await parseJsonResponse(response, 'Task status update');
3189
3826
  if (hasJsonFlag()) {
3190
3827
  printJson({ task: data.task });
3191
3828
  return;
3192
3829
  }
3193
- const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
3194
- printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
3830
+ printLine(`${labels.pastTense} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
3195
3831
  }
3196
3832
  async function setTaskReport() {
3197
3833
  const taskId = getArg('--task');
@@ -3367,10 +4003,10 @@ async function changeTeamMemberRole() {
3367
4003
  const email = getArg('--email');
3368
4004
  const role = getArg('--role');
3369
4005
  if (!teamSlug || !email || !role) {
3370
- throw new Error('Usage: orizu teams members role --team <teamSlug> --email <email> --role <admin|member>');
4006
+ throw new Error('Usage: orizu teams members role --team <teamSlug> --email <email> --role <admin|curator|judge>');
3371
4007
  }
3372
- if (!['admin', 'member'].includes(role)) {
3373
- throw new Error('role must be one of: admin, member');
4008
+ if (!['admin', 'curator', 'judge'].includes(role)) {
4009
+ throw new Error('role must be one of: admin, curator, judge');
3374
4010
  }
3375
4011
  const members = await fetchTeamMembers(teamSlug);
3376
4012
  const member = members.find(item => item.email.toLowerCase() === email.toLowerCase());
@@ -4129,12 +4765,16 @@ async function runnersExec() {
4129
4765
  throw new Error(`Runner failed for row ${row.id} with exit code ${result.status}: ${sanitizeTerminalText(boundedRunnerOutput(result.stderr || result.stdout))}`);
4130
4766
  }
4131
4767
  const runnerOutput = JSON.parse(readFileSync(outputPath, 'utf8'));
4768
+ // Spread runner output first: the scorer/prompt context identity fields
4769
+ // (row_id, version ids, metric_key) are authoritative, never runner-overridable.
4132
4770
  resultLines.push(JSON.stringify({
4771
+ ...runnerOutput,
4133
4772
  row_id: row.id,
4134
4773
  prompt_version_id: context.prompt.promptVersionId,
4135
4774
  runner_version_id: context.prompt.runnerVersionId,
4136
- ...(scorerVersion ? { scorer_version_id: context.scorer?.versionId || scorerVersion } : {}),
4137
- ...runnerOutput,
4775
+ ...(scorerVersion
4776
+ ? { scorer_version_id: context.scorer?.versionId || scorerVersion, metric_key: context.scorer?.metricKey || 'score' }
4777
+ : {}),
4138
4778
  }));
4139
4779
  }
4140
4780
  finally {
@@ -4434,6 +5074,14 @@ export async function main(rawArgs = process.argv.slice(2)) {
4434
5074
  await createTask();
4435
5075
  return;
4436
5076
  }
5077
+ if (command === 'tasks' && subcommand === 'update') {
5078
+ await updateTask();
5079
+ return;
5080
+ }
5081
+ if (command === 'tasks' && subcommand === 'discard') {
5082
+ await discardTask();
5083
+ return;
5084
+ }
4437
5085
  if (command === 'tasks' && subcommand === 'publish') {
4438
5086
  await publishTask();
4439
5087
  return;
@@ -4460,6 +5108,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
4460
5108
  await updateTaskStatus('active');
4461
5109
  return;
4462
5110
  }
5111
+ if (command === 'tasks' && subcommand === 'complete') {
5112
+ await updateTaskStatus('completed');
5113
+ return;
5114
+ }
4463
5115
  if (command === 'datasets' && subcommand === 'upload') {
4464
5116
  await uploadDataset();
4465
5117
  return;
@@ -4513,6 +5165,41 @@ export async function main(rawArgs = process.argv.slice(2)) {
4513
5165
  await downloadAnnotations();
4514
5166
  return;
4515
5167
  }
5168
+ if (command === 'internal' || (command === 'session' && subcommand === 'start' && hasArg('--hosted'))) {
5169
+ process.exitCode = await hostedCommand(cliArgs, { json: hasJsonFlag(), print: printLine, printErr: printError });
5170
+ return;
5171
+ }
5172
+ if (command === 'session' || command === 'run') {
5173
+ process.exitCode = await workbenchCommand(cliArgs, { json: hasJsonFlag(), print: printLine });
5174
+ return;
5175
+ }
5176
+ if (command === 'workspace') {
5177
+ process.exitCode = await workspaceSyncCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine });
5178
+ return;
5179
+ }
5180
+ if (command === 'git-credential') {
5181
+ process.exitCode = await runGitCredential(subcommand ?? 'get', { stdin: readFileSync(0, 'utf8'), cwd: process.cwd(), print: printLine, printErr: printError });
5182
+ return;
5183
+ }
5184
+ if (command === 'github' && subcommand === 'link') {
5185
+ const noInput = hasArg('--no-input') || hasArg('--non-interactive') || !isInteractiveTerminal();
5186
+ const team = getOptionalArgValue('--team') || (await resolveSetupTeam(null, noInput, false)).slug;
5187
+ await runGithubLink(team, { print: printLine, fetcher: authedFetch, openUrl: openInBrowser, confirm: (org) => askYesNo(`Connect this team to GitHub org '${org}'?`, true) });
5188
+ return;
5189
+ }
5190
+ if (command === 'connectors' || command === 'manifests') {
5191
+ const moduleCommand = command === 'connectors' ? connectorsCommand : manifestsCommand;
5192
+ process.exitCode = await moduleCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, resolveProjectSlug });
5193
+ return;
5194
+ }
5195
+ if (command === 'team' && (subcommand === 'kill-agents' || subcommand === 'release-agents')) {
5196
+ process.exitCode = await killSwitchCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, printErr: printError });
5197
+ return;
5198
+ }
5199
+ if (command === 'team' && subcommand === 'egress-allowlist') {
5200
+ process.exitCode = await egressAllowlistCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, printErr: printError });
5201
+ return;
5202
+ }
4516
5203
  printUsage();
4517
5204
  process.exit(1);
4518
5205
  }
@@ -4530,7 +5217,7 @@ function isCliEntrypoint() {
4530
5217
  }
4531
5218
  if (isCliEntrypoint()) {
4532
5219
  main().catch(error => {
4533
- console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'));
5220
+ printError(error instanceof Error ? error.message : 'Unknown error');
4534
5221
  process.exit(1);
4535
5222
  });
4536
5223
  }