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/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,15 +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 { parseAssignmentManifestJsonl } from './task-assignment-manifest.js';
21
- 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';
22
24
  import { computeSkillContentHash, getSkillInstallPath, getSkillTargetStatus, getTargetForAgent, installSkillTarget, isSkillInstallAgent, isSkillInstallTarget, resolveSkillSource, SKILL_INSTALL_AGENTS, SKILL_INSTALL_TARGETS, targetNeedsOverwrite, } from './skill-installer.js';
23
25
  import { renderBanner } from './banner.js';
24
26
  import { formatTaskCreateError } from './task-create-error.js';
25
27
  import { renderAgentSetupPrompt } from './setup-prompt.js';
26
- 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
+ }
27
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
+ };
28
59
  function getCliVersion() {
29
60
  const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
30
61
  if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
@@ -49,12 +80,6 @@ function printUsage() {
49
80
  printBannerIfInteractive();
50
81
  printLine(renderRootHelp());
51
82
  }
52
- function printPreviewUsage() {
53
- 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>]`);
54
- }
55
- function printOptimizationUsage() {
56
- 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]`);
57
- }
58
83
  let cliArgs = process.argv.slice(2);
59
84
  let cliJsonOutput = false;
60
85
  function getArg(name) {
@@ -79,6 +104,17 @@ function getArgs(name) {
79
104
  }
80
105
  return values;
81
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
+ }
82
118
  function rejectDashPrefixedOptionValue(name, value) {
83
119
  if (value && value.startsWith('-')) {
84
120
  throw new Error(`Invalid value for ${name}: option values cannot start with a dash`);
@@ -123,6 +159,9 @@ export function createCodeChallenge(verifier) {
123
159
  export function sanitizeTerminalText(value) {
124
160
  return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
125
161
  }
162
+ function printError(message) {
163
+ console.error(sanitizeTerminalText(message));
164
+ }
126
165
  export function validateBrowserUrl(url, expectedOrigin) {
127
166
  let parsed;
128
167
  try {
@@ -266,7 +305,7 @@ function readJsonObjectArg(valueArg, label) {
266
305
  return parsed;
267
306
  }
268
307
  catch (error) {
269
- throw new Error(`Invalid ${label} '${valueArg}': ${error?.message || String(error)}`);
308
+ throw new Error(`Invalid ${label} '${valueArg}': ${getErrorMessage(error)}`);
270
309
  }
271
310
  }
272
311
  function readJsonPayloadArg(pathArg) {
@@ -352,6 +391,38 @@ async function fetchTeamMembers(teamSlug) {
352
391
  const data = await parseJsonResponse(response, 'Team members list');
353
392
  return data.members;
354
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
+ }
355
426
  async function resolveProjectSelection(projectArg) {
356
427
  projectArg = projectArg || process.env.ORIZU_PROJECT || null;
357
428
  const teams = await fetchTeams();
@@ -371,7 +442,7 @@ async function resolveProjectSelection(projectArg) {
371
442
  const [teamSlug, projectSlug] = segments.map(normalizeSlugInput);
372
443
  const matchedTeam = teams.find(team => team.slug === teamSlug);
373
444
  if (!matchedTeam) {
374
- console.error(`Team '${sanitizeTerminalText(teamSlug)}' not found in your accessible teams.`);
445
+ printError(`Team '${teamSlug}' not found in your accessible teams.`);
375
446
  const selectedTeam = await promptSelect('Select a team', teams, team => `${team.name} (${team.slug})`);
376
447
  const projects = await fetchProjects(selectedTeam.slug);
377
448
  const selectedProject = await promptSelect(`Select a project in ${selectedTeam.slug}`, projects, project => `${project.name} (${project.teamSlug}/${project.slug})`);
@@ -380,7 +451,7 @@ async function resolveProjectSelection(projectArg) {
380
451
  const projects = await fetchProjects(matchedTeam.slug);
381
452
  const matchedProject = projects.find(project => project.slug === projectSlug);
382
453
  if (!matchedProject) {
383
- console.error(`Project '${sanitizeTerminalText(projectSlug)}' not found in team '${sanitizeTerminalText(matchedTeam.slug)}'.`);
454
+ printError(`Project '${projectSlug}' not found in team '${matchedTeam.slug}'.`);
384
455
  const selectedProject = await promptSelect(`Select a project in ${matchedTeam.slug}`, projects, project => `${project.name} (${project.teamSlug}/${project.slug})`);
385
456
  return selectedProject;
386
457
  }
@@ -1989,20 +2060,12 @@ async function createTeam() {
1989
2060
  if (!name) {
1990
2061
  throw new Error('Usage: orizu teams create --name <name>');
1991
2062
  }
1992
- const response = await authedFetch('/api/cli/teams', {
1993
- method: 'POST',
1994
- headers: { 'Content-Type': 'application/json' },
1995
- body: JSON.stringify({ name }),
1996
- });
1997
- if (!response.ok) {
1998
- throw new Error(`Failed to create team: ${await response.text()}`);
1999
- }
2000
- const data = await parseJsonResponse(response, 'Team create');
2063
+ const team = await createTeamOnServer(name);
2001
2064
  if (hasJsonFlag()) {
2002
- printJson({ team: data.team });
2065
+ printJson({ team });
2003
2066
  return;
2004
2067
  }
2005
- printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`);
2068
+ printLine(`Created team: ${sanitizeTerminalText(team.name)} (${sanitizeTerminalText(team.slug)})`);
2006
2069
  }
2007
2070
  async function listProjects() {
2008
2071
  const teamSlugArg = getArg('--team');
@@ -2025,20 +2088,12 @@ async function createProject() {
2025
2088
  const team = await promptSelect('Select a team', await fetchTeams(), item => `${item.name} (${item.slug})`, { forcePrompt: true });
2026
2089
  teamSlug = team.slug;
2027
2090
  }
2028
- const response = await authedFetch('/api/cli/projects', {
2029
- method: 'POST',
2030
- headers: { 'Content-Type': 'application/json' },
2031
- body: JSON.stringify({ teamSlug, name }),
2032
- });
2033
- if (!response.ok) {
2034
- throw new Error(`Failed to create project: ${await response.text()}`);
2035
- }
2036
- const data = await parseJsonResponse(response, 'Project create');
2091
+ const project = await createProjectOnServer(teamSlug, name);
2037
2092
  if (hasJsonFlag()) {
2038
- printJson({ project: data.project });
2093
+ printJson({ project });
2039
2094
  return;
2040
2095
  }
2041
- printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`);
2096
+ printLine(`Created project ${sanitizeTerminalText(`${project.teamSlug}/${project.slug}`)}`);
2042
2097
  }
2043
2098
  async function listTasks() {
2044
2099
  const project = getArg('--project');
@@ -2064,10 +2119,10 @@ function readSourceFile(pathArg) {
2064
2119
  return readFileSync(expandedPath, 'utf-8');
2065
2120
  }
2066
2121
  catch (error) {
2067
- if (error?.code === 'ENOENT') {
2122
+ if (isNodeError(error) && error.code === 'ENOENT') {
2068
2123
  throw new Error(`File not found: ${expandedPath}`);
2069
2124
  }
2070
- throw new Error(`Failed to read file '${expandedPath}': ${error?.message || String(error)}`);
2125
+ throw new Error(`Failed to read file '${expandedPath}': ${getErrorMessage(error)}`);
2071
2126
  }
2072
2127
  }
2073
2128
  function readSourceBytes(pathArg) {
@@ -2076,10 +2131,10 @@ function readSourceBytes(pathArg) {
2076
2131
  return readFileSync(expandedPath);
2077
2132
  }
2078
2133
  catch (error) {
2079
- if (error?.code === 'ENOENT') {
2134
+ if (isNodeError(error) && error.code === 'ENOENT') {
2080
2135
  throw new Error(`File not found: ${expandedPath}`);
2081
2136
  }
2082
- throw new Error(`Failed to read file '${expandedPath}': ${error?.message || String(error)}`);
2137
+ throw new Error(`Failed to read file '${expandedPath}': ${getErrorMessage(error)}`);
2083
2138
  }
2084
2139
  }
2085
2140
  function hasJsonFlag() {
@@ -2116,12 +2171,261 @@ async function askYesNo(question, defaultYes) {
2116
2171
  rl.close();
2117
2172
  }
2118
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
+ }
2119
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]`;
2120
2422
  function describeSkillTarget(target) {
2121
2423
  if (target === 'claude-user')
2122
2424
  return 'Claude Code, for you across all projects';
2123
2425
  if (target === 'claude-project')
2124
2426
  return 'Claude Code, only for this project';
2427
+ if (target === 'codex-user')
2428
+ return 'Codex, for you across all projects';
2125
2429
  if (target === 'agent-user')
2126
2430
  return 'Codex / Open Agent Skills, for you across all projects';
2127
2431
  if (target === 'agents-project')
@@ -2223,8 +2527,16 @@ function describePlannedWrite(target, action, mode, path) {
2223
2527
  async function applySkillInstallTargets(targets, options) {
2224
2528
  const cliVersion = getCliVersion();
2225
2529
  const jsonMode = hasJsonFlag();
2226
- const planned = targets.map(target => installSkillTarget(target, { dryRun: true, mode: options.mode, cliVersion }));
2227
- 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) {
2228
2540
  printLine('Planned writes:');
2229
2541
  for (const plan of planned) {
2230
2542
  printLine(describePlannedWrite(plan.target, plan.action, plan.mode, plan.path));
@@ -2232,7 +2544,7 @@ async function applySkillInstallTargets(targets, options) {
2232
2544
  printLine('');
2233
2545
  }
2234
2546
  if (options.dryRun) {
2235
- if (!jsonMode) {
2547
+ if (printResults && !jsonMode) {
2236
2548
  printLine('Dry run: no files were changed.');
2237
2549
  }
2238
2550
  return planned.map(plan => ({
@@ -2245,8 +2557,8 @@ async function applySkillInstallTargets(targets, options) {
2245
2557
  const outcomes = [];
2246
2558
  for (const target of targets) {
2247
2559
  let overwrite = options.skipConfirm;
2248
- const installPath = getSkillInstallPath(target);
2249
- 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 })) {
2250
2562
  if (!isInteractiveTerminal()) {
2251
2563
  throw new Error(`${installPath} already exists. Pass --yes to replace it.`);
2252
2564
  }
@@ -2261,11 +2573,13 @@ async function applySkillInstallTargets(targets, options) {
2261
2573
  }
2262
2574
  try {
2263
2575
  const result = installSkillTarget(target, {
2576
+ cwd: options.cwd,
2577
+ homeDir: options.homeDir,
2264
2578
  overwrite,
2265
2579
  mode: options.mode,
2266
2580
  cliVersion,
2267
2581
  });
2268
- if (!jsonMode) {
2582
+ if (printResults && !jsonMode) {
2269
2583
  const suffix = result.mode === 'link' ? ' (symlink)' : '';
2270
2584
  printLine(`${formatSkillInstallAction(result.action)} ${result.path}${suffix}`);
2271
2585
  }
@@ -2277,8 +2591,8 @@ async function applySkillInstallTargets(targets, options) {
2277
2591
  });
2278
2592
  }
2279
2593
  catch (error) {
2280
- const message = error?.message || String(error);
2281
- if (!jsonMode) {
2594
+ const message = getErrorMessage(error);
2595
+ if (printResults && !jsonMode) {
2282
2596
  printLine(`Failed ${installPath}: ${message}`);
2283
2597
  }
2284
2598
  outcomes.push({ target, path: installPath, action: 'failed', mode: null, error: message });
@@ -2440,15 +2754,124 @@ function describeSetupAuthState() {
2440
2754
  baseUrl,
2441
2755
  };
2442
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
+ }
2443
2856
  async function setupCommand() {
2444
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'));
2445
2862
  const skipConfirm = hasArg('--yes');
2446
- 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';
2447
2870
  printBannerIfInteractive();
2448
2871
  printLine('Orizu setup');
2449
2872
  printLine('');
2450
- // Phase 1: login
2451
- printLine('1. Authentication');
2873
+ // Step 1: login
2874
+ printLine('Step 1: Login');
2452
2875
  let auth = describeSetupAuthState();
2453
2876
  if (hasArg('--skip-login')) {
2454
2877
  printLine(' Skipped (--skip-login).');
@@ -2459,72 +2882,177 @@ async function setupCommand() {
2459
2882
  else if (noInput || dryRun) {
2460
2883
  printLine(' Not signed in. Run `orizu login` to authenticate.');
2461
2884
  }
2462
- 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.')) {
2463
2886
  await login();
2464
2887
  auth = describeSetupAuthState();
2465
2888
  }
2466
2889
  else {
2467
- printLine(' Skipped. Run `orizu login` when ready.');
2890
+ throw new Error('Setup cancelled.');
2468
2891
  }
2469
2892
  printLine('');
2470
- // Phase 2: agent integration
2471
- printLine('2. Agent integration');
2472
- const scope = parseSkillInstallScope();
2473
- const mode = parseSkillInstallMode();
2474
- let targets = hasArg('--no-install') ? [] : parseSkillInstallTargets(scope);
2475
- if (targets.length === 0 && !hasArg('--no-install')) {
2476
- if (noInput) {
2477
- 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
+ }
2478
2992
  }
2479
2993
  else {
2480
- targets = await promptSkillInstallTargets(scope);
2994
+ printLine(` Skipped. Rerun with --workspace to initialize the workspace contract.`);
2481
2995
  }
2482
2996
  }
2483
- let installOutcomes = [];
2484
- if (targets.length > 0) {
2485
- installOutcomes = await applySkillInstallTargets(targets, { mode, skipConfirm, dryRun });
2486
- }
2487
- else if (hasArg('--no-install')) {
2488
- printLine(' Skipped (--no-install).');
2489
- }
2490
2997
  printLine('');
2491
- // Phase 3: local workspace
2492
- printLine('3. Local workspace');
2493
- let workspaceState = 'skipped';
2494
- if (hasArg('--no-workspace')) {
2495
- printLine(' Skipped (--no-workspace).');
2496
- }
2497
- else if (workspaceExists()) {
2498
- workspaceState = 'exists';
2499
- 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.');
2500
3003
  }
2501
3004
  else {
2502
- const wantsWorkspace = hasArg('--workspace') ||
2503
- (!noInput && await askYesNo(` Create a gitignored ${WORKSPACE_DIR_NAME}/ workspace for local Orizu artifacts?`, true));
2504
- if (wantsWorkspace) {
2505
- const result = initOrizuWorkspace({
2506
- baseUrl: auth.baseUrl,
2507
- cliVersion: getCliVersion(),
2508
- dryRun,
2509
- });
2510
- workspaceState = result.state === 'would-create' ? 'would-create' : result.state;
2511
- for (const action of result.actions) {
2512
- 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);
3011
+ }
3012
+ else {
3013
+ printLine(' No Codex or Claude Code agent folders were detected.');
2513
3014
  }
2514
3015
  }
3016
+ if (targets.length === 0) {
3017
+ printLine(' Skipped skill install.');
3018
+ }
2515
3019
  else {
2516
- 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
+ }
2517
3045
  }
2518
3046
  }
2519
3047
  printLine('');
2520
3048
  // Phase 4: coding-agent handoff
2521
3049
  const handoffPrompt = renderAgentSetupPrompt({
2522
- workspacePath: workspaceState === 'created' || workspaceState === 'exists'
2523
- ? getWorkspaceRoot()
3050
+ workspacePath: workspaceResult && workspaceState !== 'invalid'
3051
+ ? getWorkspaceRoot(workspaceRoot)
2524
3052
  : null,
2525
3053
  });
2526
- if (!hasArg('--no-handoff')) {
2527
- printLine('4. Coding-agent handoff');
3054
+ if (handoffRequested && !hasArg('--no-handoff')) {
3055
+ printLine('Step 4: Coding-agent handoff');
2528
3056
  printLine(' Give your coding agent this prompt to plan repo-specific Orizu adoption:');
2529
3057
  printLine('');
2530
3058
  printLine('--- prompt start ---');
@@ -2564,7 +3092,7 @@ async function setupCommand() {
2564
3092
  const summaryTargets = installOutcomes.length > 0
2565
3093
  ? installOutcomes
2566
3094
  .filter(outcome => outcome.action !== 'failed')
2567
- .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target) }))
3095
+ .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target, { cwd: workspaceRoot, homeDir: setupSkillHome }) }))
2568
3096
  : [];
2569
3097
  const source = resolveSkillSource();
2570
3098
  if (hasJsonFlag()) {
@@ -2583,29 +3111,45 @@ async function setupCommand() {
2583
3111
  cliVersion: getCliVersion(),
2584
3112
  skillHash: computeSkillContentHash(source.root),
2585
3113
  },
2586
- 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
+ },
2587
3129
  });
2588
3130
  return;
2589
3131
  }
2590
3132
  printLine('Setup summary');
2591
3133
  printLine(` Auth: ${auth.state === 'signed-in' ? `signed in (${sanitizeTerminalText(auth.baseUrl)})` : 'not signed in — run `orizu login`'}`);
2592
3134
  if (summaryTargets.length > 0) {
2593
- printLine(' Integrations:');
3135
+ printLine(' Skills:');
2594
3136
  for (const { outcome, status } of summaryTargets) {
2595
3137
  const modeLabel = status.mode ? `, ${status.mode}` : '';
2596
3138
  printLine(` ${outcome.target.padEnd(16)} ${status.state}${modeLabel} ${outcome.path}`);
2597
3139
  }
2598
3140
  }
2599
3141
  else {
2600
- printLine(' Integrations: none installed this run — `orizu install-skill` sets them up.');
3142
+ printLine(' Skills: none installed this run');
2601
3143
  }
2602
3144
  const failed = installOutcomes.filter(outcome => outcome.action === 'failed');
2603
3145
  for (const failure of failed) {
2604
3146
  printLine(` ${failure.target.padEnd(16)} failed: ${failure.error}`);
2605
3147
  }
2606
- printLine(` Skill source: ${source.root} (${source.source})`);
2607
- printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : `${getWorkspaceRoot()} (${workspaceState})`}`);
2608
- 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.');
2609
3153
  }
2610
3154
  function setupPromptCommand() {
2611
3155
  const prompt = renderAgentSetupPrompt({
@@ -2627,7 +3171,7 @@ function readJsonFile(pathArg) {
2627
3171
  return parsed;
2628
3172
  }
2629
3173
  catch (error) {
2630
- throw new Error(`Invalid JSON file '${pathArg}': ${error?.message || String(error)}`);
3174
+ throw new Error(`Invalid JSON file '${pathArg}': ${getErrorMessage(error)}`);
2631
3175
  }
2632
3176
  }
2633
3177
  function isRecord(value) {
@@ -2691,126 +3235,6 @@ function writeTextFileEnsuringDir(pathArg, content) {
2691
3235
  mkdirSync(dirname(pathArg), { recursive: true });
2692
3236
  writeFileSync(pathArg, content);
2693
3237
  }
2694
- function shouldExcludeArtifactPath(relativePath) {
2695
- const parts = relativePath.split('/');
2696
- return parts.some(part => part === '.git' ||
2697
- part === '.DS_Store' ||
2698
- part === '__pycache__' ||
2699
- part === '.pytest_cache');
2700
- }
2701
- function collectArtifactFiles(sourceDir, relativeDir = '') {
2702
- const absoluteDir = relativeDir ? join(sourceDir, relativeDir) : sourceDir;
2703
- return readdirSync(absoluteDir, { withFileTypes: true })
2704
- .flatMap(entry => {
2705
- const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
2706
- if (shouldExcludeArtifactPath(relativePath)) {
2707
- return [];
2708
- }
2709
- if (entry.isDirectory()) {
2710
- return collectArtifactFiles(sourceDir, relativePath);
2711
- }
2712
- return entry.isFile() ? [relativePath] : [];
2713
- })
2714
- .sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
2715
- }
2716
- const ZIP_GENERAL_PURPOSE_UTF8 = 0x0800;
2717
- const ZIP_DOS_TIME_2000_01_01 = 0;
2718
- const ZIP_DOS_DATE_2000_01_01 = ((2000 - 1980) << 9) | (1 << 5) | 1;
2719
- const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
2720
- let value = index;
2721
- for (let bit = 0; bit < 8; bit++) {
2722
- value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1);
2723
- }
2724
- return value >>> 0;
2725
- });
2726
- function crc32(bytes) {
2727
- let crc = 0xffffffff;
2728
- for (const byte of bytes) {
2729
- crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
2730
- }
2731
- return (crc ^ 0xffffffff) >>> 0;
2732
- }
2733
- function createStoredZip(entries) {
2734
- const localParts = [];
2735
- const centralParts = [];
2736
- let offset = 0;
2737
- for (const entry of entries) {
2738
- const name = Buffer.from(entry.path, 'utf8');
2739
- const crc = crc32(entry.data);
2740
- const localHeader = Buffer.alloc(30);
2741
- localHeader.writeUInt32LE(0x04034b50, 0);
2742
- localHeader.writeUInt16LE(10, 4);
2743
- localHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 6);
2744
- localHeader.writeUInt16LE(0, 8);
2745
- localHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 10);
2746
- localHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 12);
2747
- localHeader.writeUInt32LE(crc, 14);
2748
- localHeader.writeUInt32LE(entry.data.length, 18);
2749
- localHeader.writeUInt32LE(entry.data.length, 22);
2750
- localHeader.writeUInt16LE(name.length, 26);
2751
- localHeader.writeUInt16LE(0, 28);
2752
- localParts.push(localHeader, name, entry.data);
2753
- const centralHeader = Buffer.alloc(46);
2754
- centralHeader.writeUInt32LE(0x02014b50, 0);
2755
- centralHeader.writeUInt16LE(20, 4);
2756
- centralHeader.writeUInt16LE(10, 6);
2757
- centralHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 8);
2758
- centralHeader.writeUInt16LE(0, 10);
2759
- centralHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 12);
2760
- centralHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 14);
2761
- centralHeader.writeUInt32LE(crc, 16);
2762
- centralHeader.writeUInt32LE(entry.data.length, 20);
2763
- centralHeader.writeUInt32LE(entry.data.length, 24);
2764
- centralHeader.writeUInt16LE(name.length, 28);
2765
- centralHeader.writeUInt16LE(0, 30);
2766
- centralHeader.writeUInt16LE(0, 32);
2767
- centralHeader.writeUInt16LE(0, 34);
2768
- centralHeader.writeUInt16LE(0, 36);
2769
- centralHeader.writeUInt32LE(0, 38);
2770
- centralHeader.writeUInt32LE(offset, 42);
2771
- centralParts.push(centralHeader, name);
2772
- offset += localHeader.length + name.length + entry.data.length;
2773
- }
2774
- const centralDirectoryOffset = offset;
2775
- const centralDirectory = Buffer.concat(centralParts);
2776
- const endOfCentralDirectory = Buffer.alloc(22);
2777
- endOfCentralDirectory.writeUInt32LE(0x06054b50, 0);
2778
- endOfCentralDirectory.writeUInt16LE(0, 4);
2779
- endOfCentralDirectory.writeUInt16LE(0, 6);
2780
- endOfCentralDirectory.writeUInt16LE(entries.length, 8);
2781
- endOfCentralDirectory.writeUInt16LE(entries.length, 10);
2782
- endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12);
2783
- endOfCentralDirectory.writeUInt32LE(centralDirectoryOffset, 16);
2784
- endOfCentralDirectory.writeUInt16LE(0, 20);
2785
- return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory]);
2786
- }
2787
- function zipDirectoryToBase64(dirArg) {
2788
- const sourceDir = expandHomePath(dirArg);
2789
- try {
2790
- const stats = statSync(sourceDir);
2791
- if (!stats.isDirectory()) {
2792
- throw new Error(`${sourceDir} is not a directory`);
2793
- }
2794
- }
2795
- catch (error) {
2796
- if (error?.code === 'ENOENT') {
2797
- throw new Error(`Directory not found: ${sourceDir}`);
2798
- }
2799
- throw error;
2800
- }
2801
- const files = collectArtifactFiles(sourceDir);
2802
- if (files.length === 0) {
2803
- throw new Error(`Directory contains no artifact files: ${sourceDir}`);
2804
- }
2805
- const bytes = createStoredZip(files.map(relativePath => ({
2806
- path: relativePath,
2807
- data: readFileSync(join(sourceDir, relativePath)),
2808
- })));
2809
- return {
2810
- zipBase64: bytes.toString('base64'),
2811
- contentSha256: createHash('sha256').update(bytes).digest('hex'),
2812
- };
2813
- }
2814
3238
  function readManifestFile(dirArg) {
2815
3239
  return readJsonFile(join(expandHomePath(dirArg), 'manifest.json'));
2816
3240
  }
@@ -2991,8 +3415,32 @@ function readAssignmentManifest(path) {
2991
3415
  if (!path) {
2992
3416
  return null;
2993
3417
  }
2994
- const content = readFileSync(expandHomePath(path), 'utf8');
2995
- return parseAssignmentManifestJsonl(content);
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;
2996
3444
  }
2997
3445
  async function createTask() {
2998
3446
  const projectSlug = getArg('--project');
@@ -3103,6 +3551,127 @@ async function createTask() {
3103
3551
  : `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`}`
3104
3552
  : ''));
3105
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
+ }
3106
3675
  async function publishTask() {
3107
3676
  const taskId = getArg('--task');
3108
3677
  const assignees = parseCommaSeparated(getArg('--assignees'));
@@ -3241,9 +3810,9 @@ async function taskStatus() {
3241
3810
  }
3242
3811
  async function updateTaskStatus(targetStatus) {
3243
3812
  const taskId = getArg('--task');
3813
+ const labels = TASK_STATUS_ACTION_LABELS[targetStatus];
3244
3814
  if (!taskId) {
3245
- const verb = targetStatus === 'paused' ? 'pause' : 'unpause';
3246
- throw new Error(`Usage: orizu tasks ${verb} --task <taskId>`);
3815
+ throw new Error(`Usage: orizu tasks ${labels.verb} --task <taskId>`);
3247
3816
  }
3248
3817
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`, {
3249
3818
  method: 'PATCH',
@@ -3251,16 +3820,14 @@ async function updateTaskStatus(targetStatus) {
3251
3820
  body: JSON.stringify({ status: targetStatus }),
3252
3821
  });
3253
3822
  if (!response.ok) {
3254
- const verb = targetStatus === 'paused' ? 'pause' : 'unpause';
3255
- throw new Error(`Failed to ${verb} task: ${await response.text()}`);
3823
+ throw new Error(`Failed to ${labels.verb} task: ${await response.text()}`);
3256
3824
  }
3257
3825
  const data = await parseJsonResponse(response, 'Task status update');
3258
3826
  if (hasJsonFlag()) {
3259
3827
  printJson({ task: data.task });
3260
3828
  return;
3261
3829
  }
3262
- const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
3263
- printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
3830
+ printLine(`${labels.pastTense} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
3264
3831
  }
3265
3832
  async function setTaskReport() {
3266
3833
  const taskId = getArg('--task');
@@ -3436,10 +4003,10 @@ async function changeTeamMemberRole() {
3436
4003
  const email = getArg('--email');
3437
4004
  const role = getArg('--role');
3438
4005
  if (!teamSlug || !email || !role) {
3439
- 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>');
3440
4007
  }
3441
- if (!['admin', 'member'].includes(role)) {
3442
- 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');
3443
4010
  }
3444
4011
  const members = await fetchTeamMembers(teamSlug);
3445
4012
  const member = members.find(item => item.email.toLowerCase() === email.toLowerCase());
@@ -4198,12 +4765,16 @@ async function runnersExec() {
4198
4765
  throw new Error(`Runner failed for row ${row.id} with exit code ${result.status}: ${sanitizeTerminalText(boundedRunnerOutput(result.stderr || result.stdout))}`);
4199
4766
  }
4200
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.
4201
4770
  resultLines.push(JSON.stringify({
4771
+ ...runnerOutput,
4202
4772
  row_id: row.id,
4203
4773
  prompt_version_id: context.prompt.promptVersionId,
4204
4774
  runner_version_id: context.prompt.runnerVersionId,
4205
- ...(scorerVersion ? { scorer_version_id: context.scorer?.versionId || scorerVersion } : {}),
4206
- ...runnerOutput,
4775
+ ...(scorerVersion
4776
+ ? { scorer_version_id: context.scorer?.versionId || scorerVersion, metric_key: context.scorer?.metricKey || 'score' }
4777
+ : {}),
4207
4778
  }));
4208
4779
  }
4209
4780
  finally {
@@ -4503,6 +5074,14 @@ export async function main(rawArgs = process.argv.slice(2)) {
4503
5074
  await createTask();
4504
5075
  return;
4505
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
+ }
4506
5085
  if (command === 'tasks' && subcommand === 'publish') {
4507
5086
  await publishTask();
4508
5087
  return;
@@ -4529,6 +5108,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
4529
5108
  await updateTaskStatus('active');
4530
5109
  return;
4531
5110
  }
5111
+ if (command === 'tasks' && subcommand === 'complete') {
5112
+ await updateTaskStatus('completed');
5113
+ return;
5114
+ }
4532
5115
  if (command === 'datasets' && subcommand === 'upload') {
4533
5116
  await uploadDataset();
4534
5117
  return;
@@ -4582,6 +5165,41 @@ export async function main(rawArgs = process.argv.slice(2)) {
4582
5165
  await downloadAnnotations();
4583
5166
  return;
4584
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
+ }
4585
5203
  printUsage();
4586
5204
  process.exit(1);
4587
5205
  }
@@ -4599,7 +5217,7 @@ function isCliEntrypoint() {
4599
5217
  }
4600
5218
  if (isCliEntrypoint()) {
4601
5219
  main().catch(error => {
4602
- console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'));
5220
+ printError(error instanceof Error ? error.message : 'Unknown error');
4603
5221
  process.exit(1);
4604
5222
  });
4605
5223
  }