orizu 0.5.24 → 0.5.26

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 (57) hide show
  1. package/README.md +23 -3
  2. package/dist/credentials.d.ts +15 -0
  3. package/dist/credentials.d.ts.map +1 -1
  4. package/dist/credentials.js +279 -59
  5. package/dist/help.d.ts.map +1 -1
  6. package/dist/help.js +5 -2
  7. package/dist/http.d.ts +20 -0
  8. package/dist/http.d.ts.map +1 -1
  9. package/dist/http.js +239 -19
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +153 -213
  12. package/dist/keyboard-select.d.ts +7 -0
  13. package/dist/keyboard-select.d.ts.map +1 -0
  14. package/dist/keyboard-select.js +72 -0
  15. package/dist/logout-diagnostic.d.ts +4 -0
  16. package/dist/logout-diagnostic.d.ts.map +1 -0
  17. package/dist/logout-diagnostic.js +26 -0
  18. package/dist/setup-account.d.ts +11 -0
  19. package/dist/setup-account.d.ts.map +1 -0
  20. package/dist/setup-account.js +106 -0
  21. package/dist/setup-onboarding.d.ts +27 -1
  22. package/dist/setup-onboarding.d.ts.map +1 -1
  23. package/dist/setup-onboarding.js +95 -11
  24. package/dist/setup-summary.d.ts +12 -0
  25. package/dist/setup-summary.d.ts.map +1 -0
  26. package/dist/setup-summary.js +56 -0
  27. package/package.json +1 -1
  28. package/src/credentials.ts +326 -57
  29. package/src/help.ts +7 -2
  30. package/src/http.ts +325 -20
  31. package/src/index.ts +193 -279
  32. package/src/keyboard-select.ts +97 -0
  33. package/src/logout-diagnostic.ts +39 -0
  34. package/src/setup-account.ts +129 -0
  35. package/src/setup-onboarding.ts +133 -10
  36. package/src/setup-summary.ts +77 -0
  37. package/vendor/skills/orizu/SKILL.md +51 -111
  38. package/vendor/skills/orizu/flows/expand.md +27 -0
  39. package/vendor/skills/orizu/flows/first-win.md +31 -0
  40. package/vendor/skills/orizu/flows/onboard.md +31 -0
  41. package/vendor/skills/orizu/flows/promote.md +33 -0
  42. package/vendor/skills/orizu/flows/recurse.md +23 -0
  43. package/vendor/skills/orizu/flows/triage.md +37 -0
  44. package/vendor/skills/orizu/references/assess-and-plan.md +5 -5
  45. package/vendor/skills/orizu/references/authority-map.md +20 -0
  46. package/vendor/skills/orizu/references/building-apps.md +7 -1
  47. package/vendor/skills/orizu/references/building-judges.md +15 -16
  48. package/vendor/skills/orizu/references/cli-reference.md +17 -8
  49. package/vendor/skills/orizu/references/dataset-design.md +16 -6
  50. package/vendor/skills/orizu/references/eval-strategy.md +18 -15
  51. package/vendor/skills/orizu/references/instructions-after-prompts.md +2 -2
  52. package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +10 -6
  53. package/vendor/skills/orizu/references/optimization-reports.md +11 -11
  54. package/vendor/skills/orizu/references/optimization-with-gepa.md +4 -2
  55. package/vendor/skills/orizu/references/primer.md +18 -22
  56. package/vendor/skills/orizu/references/prompt-control-plane.md +5 -5
  57. package/vendor/skills/orizu/references/vocabulary.md +27 -0
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, rel
4
4
  import { fileURLToPath } from 'url';
5
5
  import { createServer } from 'http';
6
6
  import { gunzipSync, gzipSync } from 'zlib';
7
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'fs';
7
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'fs';
8
8
  import { spawn, spawnSync } from 'child_process';
9
- import { homedir, tmpdir } from 'os';
9
+ import { tmpdir } from 'os';
10
10
  import { createInterface } from 'readline/promises';
11
11
  import { emitKeypressEvents } from 'readline';
12
12
  import { stdin as input, stdout as output } from 'process';
@@ -18,17 +18,21 @@ import { parseDatasetReference } from './dataset-download.js';
18
18
  import { editDatasetRows as runEditDatasetRows } from './dataset-edit-rows.js';
19
19
  import { ensureDatasetUploadSnapshot } from './dataset-upload-snapshot.js';
20
20
  import { extractErrorMessage } from './error-response.js';
21
+ import { describeLogoutHttpFailure, describeLogoutTransportFailure, } from './logout-diagnostic.js';
21
22
  import { parseGlobalFlags } from './global-flags.js';
22
23
  import { shouldUseHeadlessLogin, waitForHeadlessAuthorization } from './headless-login.js';
23
24
  import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js';
24
25
  import { runLocalAppPreview } from './preview-runtime.js';
25
26
  import { readAssignmentManifestJsonlFile } from './task-assignment-manifest.js';
26
- import { assertSecureTokenTransport, authedFetch, credentialRequestRedirectPolicy, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags, } from './http.js';
27
+ import { assertSecureTokenTransport, authedFetch, captureAuthenticatedRequestContext, credentialRequestRedirectPolicy, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags, } from './http.js';
27
28
  import { computeSkillContentHash, getSkillInstallPath, getSkillTargetStatus, getTargetForAgent, installSkillTarget, isSkillInstallAgent, isSkillInstallTarget, resolveSkillSource, SKILL_INSTALL_AGENTS, SKILL_INSTALL_TARGETS, targetNeedsOverwrite, } from './skill-installer.js';
28
29
  import { renderBanner } from './banner.js';
29
30
  import { formatTaskCreateError } from './task-create-error.js';
30
31
  import { renderAgentSetupPrompt } from './setup-prompt.js';
31
- import { findExecutable, LAUNCHABLE_AGENTS, setupLaunchPrompt, validateSetupAgentArgs, validateSetupFlagValues, validateSetupSelectionArgs } from './setup-onboarding.js';
32
+ import { AGENT_LAUNCH_SPECS, detectedSetupSkillChoices, findExecutable, resolveSetupSkillHome, setupLaunchPrompt, setupNativeTargetForAgent, validateSetupAgentArgs, validateSetupFlagValues, validateSetupSelectionArgs, } from './setup-onboarding.js';
33
+ import { promptKeyboardSelect } from './keyboard-select.js';
34
+ import { confirmExistingSetupAccount, describeSetupAuthState } from './setup-account.js';
35
+ import { renderSetupSkillSummary, sanitizeSetupHumanInlineText } from './setup-summary.js';
32
36
  import { getWorkspaceRoot, inspectExistingWorkspaceTeam, initOrizuWorkspace, workspaceExists, } from './workspace.js';
33
37
  import { connectorsCommand } from './connectors-cli.js';
34
38
  import { modelConfigsCommand } from './model-configs-cli.js';
@@ -332,18 +336,18 @@ async function promptSelect(title, items, label, options) {
332
336
  rl.close();
333
337
  }
334
338
  }
335
- async function fetchTeams() {
336
- const response = await authedFetch('/api/cli/teams');
339
+ async function fetchTeams(request = authedFetch) {
340
+ const response = await request('/api/cli/teams');
337
341
  if (!response.ok) {
338
342
  throw new Error(`Failed to fetch teams: ${await response.text()}`);
339
343
  }
340
344
  const data = await parseJsonResponse(response, 'Teams list');
341
345
  return data.teams;
342
346
  }
343
- async function fetchProjects(teamSlug) {
347
+ async function fetchProjects(teamSlug, request = authedFetch) {
344
348
  const normalizedTeamSlug = teamSlug ? normalizeSlugInput(teamSlug) : undefined;
345
349
  const query = normalizedTeamSlug ? `?teamSlug=${encodeURIComponent(normalizedTeamSlug)}` : '';
346
- const response = await authedFetch(`/api/cli/projects${query}`);
350
+ const response = await request(`/api/cli/projects${query}`);
347
351
  if (!response.ok) {
348
352
  throw new Error(`Failed to fetch projects: ${await response.text()}`);
349
353
  }
@@ -394,8 +398,8 @@ async function fetchTeamMembers(teamSlug) {
394
398
  const data = await parseJsonResponse(response, 'Team members list');
395
399
  return data.members;
396
400
  }
397
- async function createTeamOnServer(name) {
398
- const response = await authedFetch('/api/cli/teams', {
401
+ async function createTeamOnServer(name, request = authedFetch) {
402
+ const response = await request('/api/cli/teams', {
399
403
  method: 'POST',
400
404
  headers: { 'Content-Type': 'application/json' },
401
405
  body: JSON.stringify({ name }),
@@ -406,8 +410,8 @@ async function createTeamOnServer(name) {
406
410
  const data = await parseJsonResponse(response, 'Team create');
407
411
  return data.team;
408
412
  }
409
- async function createProjectOnServer(teamSlug, name) {
410
- const response = await authedFetch('/api/cli/projects', {
413
+ async function createProjectOnServer(teamSlug, name, request = authedFetch) {
414
+ const response = await request('/api/cli/projects', {
411
415
  method: 'POST',
412
416
  headers: { 'Content-Type': 'application/json' },
413
417
  body: JSON.stringify({ teamSlug, name }),
@@ -582,8 +586,9 @@ function resolveAuthCallbackPort() {
582
586
  }
583
587
  return parsed;
584
588
  }
585
- async function login() {
586
- const baseUrl = hasArg('--no-prompt-if-logged-in') ? getBaseUrl() : resolveLoginBaseUrl();
589
+ async function login(baseUrlOverride) {
590
+ const baseUrl = baseUrlOverride
591
+ ?? (hasArg('--no-prompt-if-logged-in') ? getBaseUrl() : resolveLoginBaseUrl());
587
592
  assertSecureTokenTransport(baseUrl);
588
593
  // hasResolvableAuth (ALI-1090): an env bearer (ORIZU_TOKEN / ORIZU_TOKEN_FILE)
589
594
  // counts as logged in — a pre-authenticated hosted sandbox must not fall into
@@ -701,8 +706,8 @@ async function whoami() {
701
706
  }
702
707
  printLine(sanitizeTerminalText(data.user.email ?? data.user.id));
703
708
  }
704
- async function logout() {
705
- const baseUrl = getBaseUrl();
709
+ async function logout(baseUrlOverride) {
710
+ const baseUrl = baseUrlOverride || getBaseUrl();
706
711
  const credentials = getServerCredentials(baseUrl);
707
712
  if (!credentials) {
708
713
  if (hasJsonFlag()) {
@@ -712,6 +717,13 @@ async function logout() {
712
717
  printLine(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`);
713
718
  return;
714
719
  }
720
+ const authorizationToken = 'accessToken' in credentials
721
+ ? credentials.accessToken
722
+ : credentials.apiKey;
723
+ const logoutSecrets = [
724
+ authorizationToken,
725
+ ...('refreshToken' in credentials ? [credentials.refreshToken] : []),
726
+ ];
715
727
  let remoteLogoutError = null;
716
728
  try {
717
729
  assertSecureTokenTransport(baseUrl);
@@ -719,7 +731,7 @@ async function logout() {
719
731
  method: 'POST',
720
732
  headers: {
721
733
  'Content-Type': 'application/json',
722
- Authorization: `Bearer ${'accessToken' in credentials ? credentials.accessToken : credentials.apiKey}`,
734
+ Authorization: `Bearer ${authorizationToken}`,
723
735
  },
724
736
  body: 'refreshToken' in credentials
725
737
  ? JSON.stringify({ refreshToken: credentials.refreshToken })
@@ -727,12 +739,11 @@ async function logout() {
727
739
  ...credentialRequestRedirectPolicy(),
728
740
  });
729
741
  if (!response.ok) {
730
- remoteLogoutError = sanitizeTerminalText(await response.text()).slice(0, 180);
742
+ remoteLogoutError = await describeLogoutHttpFailure(response, logoutSecrets);
731
743
  }
732
744
  }
733
745
  catch (error) {
734
- const message = error instanceof Error ? error.message : String(error);
735
- remoteLogoutError = sanitizeTerminalText(message).slice(0, 180);
746
+ remoteLogoutError = describeLogoutTransportFailure(error, logoutSecrets);
736
747
  }
737
748
  clearServerCredentials(baseUrl);
738
749
  if (hasJsonFlag()) {
@@ -1965,75 +1976,6 @@ async function waitForAnyKeyOrEscape(message) {
1965
1976
  rawInput.setRawMode?.(true);
1966
1977
  });
1967
1978
  }
1968
- async function promptKeyboardSelect(title, items, label) {
1969
- if (items.length === 0) {
1970
- throw new Error(`No options available for ${title.toLowerCase()}`);
1971
- }
1972
- if (!isInteractiveTerminal()) {
1973
- throw new Error(`${title} selection requires interactive terminal. Provide flags explicitly instead.`);
1974
- }
1975
- printLine(`\n${sanitizeTerminalText(title)}`);
1976
- printLine('Use ↑/↓ to choose, Enter to confirm, or Esc to cancel.');
1977
- let selected = 0;
1978
- let rendered = false;
1979
- const rawInput = input;
1980
- const wasRaw = Boolean(rawInput.isRaw);
1981
- emitKeypressEvents(input);
1982
- function render() {
1983
- if (rendered) {
1984
- output.write(`\x1b[${items.length}A`);
1985
- }
1986
- items.forEach((item, index) => {
1987
- output.write('\x1b[2K');
1988
- const marker = index === selected ? '●' : '○';
1989
- output.write(` ${marker} ${sanitizeTerminalText(label(item, index))}\n`);
1990
- });
1991
- rendered = true;
1992
- }
1993
- render();
1994
- output.write('\x1b[?25l');
1995
- return new Promise((resolvePromise, rejectPromise) => {
1996
- function cleanup() {
1997
- input.off('keypress', onKeypress);
1998
- if (rawInput.setRawMode && !wasRaw) {
1999
- rawInput.setRawMode(false);
2000
- }
2001
- input.pause();
2002
- output.write('\x1b[?25h');
2003
- printLine('');
2004
- }
2005
- function onKeypress(_chunk, key) {
2006
- if (key.ctrl && key.name === 'c') {
2007
- cleanup();
2008
- rejectPromise(new Error('Setup cancelled.'));
2009
- return;
2010
- }
2011
- if (key.name === 'escape') {
2012
- cleanup();
2013
- rejectPromise(new Error('Setup cancelled.'));
2014
- return;
2015
- }
2016
- if (key.name === 'up') {
2017
- selected = (selected - 1 + items.length) % items.length;
2018
- render();
2019
- return;
2020
- }
2021
- if (key.name === 'down') {
2022
- selected = (selected + 1) % items.length;
2023
- render();
2024
- return;
2025
- }
2026
- if (key.name === 'return' || key.name === 'enter') {
2027
- const item = items[selected];
2028
- cleanup();
2029
- resolvePromise(item);
2030
- }
2031
- }
2032
- input.on('keypress', onKeypress);
2033
- input.resume();
2034
- rawInput.setRawMode?.(true);
2035
- });
2036
- }
2037
1979
  async function promptKeyboardMultiSelect(title, items, label, defaultSelected) {
2038
1980
  if (items.length === 0) {
2039
1981
  return [];
@@ -2162,13 +2104,13 @@ function writeWorkspaceValidationLog(workspaceRoot, findings, dryRun) {
2162
2104
  }
2163
2105
  function printWorkspaceFindings(findings) {
2164
2106
  if (findings.length === 0) {
2165
- printLine(' Validation passed.');
2107
+ printLine('Validation passed.');
2166
2108
  return;
2167
2109
  }
2168
- printLine(' Validation findings:');
2110
+ printLine('Validation findings:');
2169
2111
  for (const finding of findings) {
2170
2112
  const path = finding.path ? ` ${finding.path}` : '';
2171
- printLine(` - ${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`);
2113
+ printLine(`- ${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`);
2172
2114
  }
2173
2115
  }
2174
2116
  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]`;
@@ -2495,15 +2437,7 @@ function skillsPathCommand() {
2495
2437
  }
2496
2438
  printLine(hasArg('--skill-md') ? source.skillMd : source.root);
2497
2439
  }
2498
- function describeSetupAuthState() {
2499
- const baseUrl = getBaseUrl();
2500
- return {
2501
- // ALI-1090: an env bearer (ORIZU_TOKEN / ORIZU_TOKEN_FILE) is signed-in too.
2502
- state: hasResolvableAuth(baseUrl) ? 'signed-in' : 'signed-out',
2503
- baseUrl,
2504
- };
2505
- }
2506
- async function resolveSetupTeam(teamSlugArg, createTeamName, noInput, dryRun, workspaceTeamSlug) {
2440
+ async function resolveSetupTeam(teamSlugArg, createTeamName, noInput, dryRun, workspaceTeamSlug, request = authedFetch) {
2507
2441
  if (teamSlugArg && createTeamName) {
2508
2442
  throw new Error('Use either --team <slug> or --create-team <name>, not both.');
2509
2443
  }
@@ -2516,11 +2450,11 @@ async function resolveSetupTeam(teamSlugArg, createTeamName, noInput, dryRun, wo
2516
2450
  const name = createTeamName.trim();
2517
2451
  if (!name)
2518
2452
  throw new Error('--create-team requires a non-empty team name.');
2519
- const team = await createTeamOnServer(name);
2520
- printLine(` Created team ${sanitizeTerminalText(team.slug)}`);
2453
+ const team = await createTeamOnServer(name, request);
2454
+ printLine(`Created team ${sanitizeTerminalText(team.slug)}`);
2521
2455
  return team;
2522
2456
  }
2523
- const teams = await fetchTeams();
2457
+ const teams = await fetchTeams(request);
2524
2458
  const normalizedTeamSlug = teamSlugArg ? normalizeSlugInput(teamSlugArg) : null;
2525
2459
  if (normalizedTeamSlug) {
2526
2460
  const team = teams.find(candidate => candidate.slug === normalizedTeamSlug);
@@ -2542,17 +2476,17 @@ async function resolveSetupTeam(teamSlugArg, createTeamName, noInput, dryRun, wo
2542
2476
  if (choice.kind === 'team') {
2543
2477
  return choice.team;
2544
2478
  }
2545
- const name = await askText(' Team name?', '');
2479
+ const name = await askText('Team name?', '');
2546
2480
  if (!name) {
2547
2481
  throw new Error('Team name is required to create a team.');
2548
2482
  }
2549
2483
  if (workspaceTeamSlug)
2550
2484
  throw new Error(`This directory is already an Orizu workspace for team '${workspaceTeamSlug}'. Create the new team from another directory.`);
2551
- const team = await createTeamOnServer(name);
2552
- printLine(` Created team ${sanitizeTerminalText(team.slug)}`);
2485
+ const team = await createTeamOnServer(name, request);
2486
+ printLine(`Created team ${sanitizeTerminalText(team.slug)}`);
2553
2487
  return team;
2554
2488
  }
2555
- async function resolveSetupProjects(team, projectSlugArg, createProjectName, noInput, dryRun) {
2489
+ async function resolveSetupProjects(team, projectSlugArg, createProjectName, noInput, dryRun, request) {
2556
2490
  if (projectSlugArg && createProjectName) {
2557
2491
  throw new Error('Use either --project <slug> or --create-project <name>, not both.');
2558
2492
  }
@@ -2563,7 +2497,7 @@ async function resolveSetupProjects(team, projectSlugArg, createProjectName, noI
2563
2497
  if (createProjectName && !canCreateProject) {
2564
2498
  throw new Error(`Only team admins can create projects in '${team.slug}'. Choose an existing project or ask a team admin to create one.`);
2565
2499
  }
2566
- const projects = await fetchProjects(team.slug);
2500
+ const projects = await fetchProjects(team.slug, request);
2567
2501
  if (createProjectName) {
2568
2502
  if (dryRun) {
2569
2503
  throw new Error('--create-project cannot be used with --dry-run because setup cannot preview a server-generated project slug.');
@@ -2571,8 +2505,8 @@ async function resolveSetupProjects(team, projectSlugArg, createProjectName, noI
2571
2505
  const name = createProjectName.trim();
2572
2506
  if (!name)
2573
2507
  throw new Error('--create-project requires a non-empty project name.');
2574
- const selected = await createProjectOnServer(team.slug, name);
2575
- printLine(` Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`);
2508
+ const selected = await createProjectOnServer(team.slug, name, request);
2509
+ printLine(`Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`);
2576
2510
  return { selected, projects: [...projects, selected] };
2577
2511
  }
2578
2512
  const normalizedProjectSlug = projectSlugArg ? normalizeSlugInput(projectSlugArg) : null;
@@ -2586,12 +2520,12 @@ async function resolveSetupProjects(team, projectSlugArg, createProjectName, noI
2586
2520
  if (projects.length === 0) {
2587
2521
  if (!canCreateProject)
2588
2522
  throw new Error(`Team '${team.slug}' has no accessible projects, and only team admins can create one.`);
2589
- printLine(` Team '${sanitizeTerminalText(team.slug)}' has no projects yet. Let's create the first one.`);
2590
- const name = await askText(' Project name?', '');
2523
+ printLine(`Team '${sanitizeTerminalText(team.slug)}' has no projects yet. Let's create the first one.`);
2524
+ const name = await askText('Project name?', '');
2591
2525
  if (!name)
2592
2526
  throw new Error('Project name is required to create a project.');
2593
- const selected = await createProjectOnServer(team.slug, name);
2594
- printLine(` Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`);
2527
+ const selected = await createProjectOnServer(team.slug, name, request);
2528
+ printLine(`Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`);
2595
2529
  return { selected, projects: [selected] };
2596
2530
  }
2597
2531
  const choices = [
@@ -2604,11 +2538,11 @@ async function resolveSetupProjects(team, projectSlugArg, createProjectName, noI
2604
2538
  if (choice.kind === 'project') {
2605
2539
  return { selected: choice.project, projects };
2606
2540
  }
2607
- const name = await askText(' Project name?', '');
2541
+ const name = await askText('Project name?', '');
2608
2542
  if (!name)
2609
2543
  throw new Error('Project name is required to create a project.');
2610
- const selected = await createProjectOnServer(team.slug, name);
2611
- printLine(` Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`);
2544
+ const selected = await createProjectOnServer(team.slug, name, request);
2545
+ printLine(`Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`);
2612
2546
  return { selected, projects: [...projects, selected] };
2613
2547
  }
2614
2548
  function projectSeedsFromProjects(projects) {
@@ -2621,32 +2555,6 @@ function projectSeedsFromProjects(projects) {
2621
2555
  function formatProjectSetupProgress(count) {
2622
2556
  return `${count} project${count === 1 ? '' : 's'} being set up in workspace...`;
2623
2557
  }
2624
- const SETUP_NATIVE_SKILL_CHOICES = [
2625
- { agent: 'claude', label: 'Claude Code', directory: ['.claude'], binary: 'claude' },
2626
- { agent: 'devin', label: 'Devin', directory: ['.devin', 'skills'] },
2627
- { agent: 'droid', label: 'Droid', directory: ['.factory', 'skills'] },
2628
- { agent: 'grok', label: 'Grok Build', directory: ['.grok', 'skills'] },
2629
- { agent: 'windsurf', label: 'Windsurf', directory: ['.windsurf', 'skills'] },
2630
- { agent: 'opencode', label: 'OpenCode', directory: ['.config', 'opencode', 'skills'], binary: 'opencode' },
2631
- ];
2632
- function setupNativeTargetForAgent(agent) {
2633
- if (agent === 'codex' || agent === 'pi')
2634
- return null;
2635
- return getTargetForAgent(agent, 'global');
2636
- }
2637
- function resolveSetupSkillHome() {
2638
- return resolve(process.env.HOME || homedir());
2639
- }
2640
- function detectedSetupSkillChoices(homeDir) {
2641
- return SETUP_NATIVE_SKILL_CHOICES
2642
- .filter(choice => existsSync(join(homeDir, ...choice.directory))
2643
- || Boolean(choice.binary && findExecutable(choice.binary)))
2644
- .map(choice => ({
2645
- agent: choice.agent,
2646
- label: choice.label,
2647
- target: getTargetForAgent(choice.agent, 'global'),
2648
- }));
2649
- }
2650
2558
  function setupSkillTargetsFromArgs() {
2651
2559
  const targets = [];
2652
2560
  for (const agent of getArgs('--agent')) {
@@ -2706,22 +2614,58 @@ async function setupCommand() {
2706
2614
  // Step 1: login
2707
2615
  printLine('Step 1: Login');
2708
2616
  let auth = describeSetupAuthState();
2617
+ let setupAuthContext = null;
2709
2618
  if (hasArg('--skip-login')) {
2710
- printLine(' Skipped (--skip-login).');
2619
+ printLine('Skipped (--skip-login).');
2711
2620
  }
2712
2621
  else if (auth.state === 'signed-in') {
2713
- printLine(` Already authenticated with ${sanitizeTerminalText(auth.baseUrl)}.`);
2622
+ if (!validateOnly) {
2623
+ setupAuthContext = captureAuthenticatedRequestContext({ allowRefresh: !dryRun });
2624
+ }
2625
+ if (!noInput && !validateOnly) {
2626
+ const confirmation = await confirmExistingSetupAccount(setupAuthContext, email => promptKeyboardSelect('Confirm the account to use for setup', ['continue', 'switch'], item => item === 'continue'
2627
+ ? `Continue as ${email}`
2628
+ : 'Log in with a different account'), async () => {
2629
+ const context = setupAuthContext;
2630
+ const logoutResult = await context.logout();
2631
+ if (logoutResult.remoteError) {
2632
+ const localOutcome = logoutResult.localCredentialCleared
2633
+ ? 'Local credentials were cleared.'
2634
+ : 'Local credentials were not cleared because a newer login is active.';
2635
+ console.warn(`Warning: remote logout failed: ${logoutResult.remoteError} ${localOutcome}`);
2636
+ }
2637
+ else if (!logoutResult.localCredentialCleared) {
2638
+ console.warn('Warning: local credentials were not cleared because a newer login is active.');
2639
+ }
2640
+ printLine(logoutResult.localCredentialCleared
2641
+ ? `Logged out from ${sanitizeTerminalText(context.baseUrl)}.`
2642
+ : `Kept the newer login for ${sanitizeTerminalText(context.baseUrl)}.`);
2643
+ await login(context.baseUrl);
2644
+ }, { dryRun });
2645
+ if (confirmation === 'switched') {
2646
+ setupAuthContext = captureAuthenticatedRequestContext({ allowRefresh: !dryRun });
2647
+ }
2648
+ auth = { state: 'signed-in', baseUrl: setupAuthContext.baseUrl };
2649
+ }
2650
+ else {
2651
+ printLine(`Already authenticated with ${sanitizeTerminalText(auth.baseUrl)}.`);
2652
+ }
2714
2653
  }
2715
2654
  else if (noInput || dryRun) {
2716
- printLine(' Not signed in. Run `orizu login` to authenticate.');
2655
+ printLine('Not signed in. Run `orizu login` to authenticate.');
2717
2656
  }
2718
- else if (await waitForAnyKeyOrEscape(' Press any key to connect your account, or Esc to cancel.')) {
2657
+ else if (await waitForAnyKeyOrEscape('Press any key to connect your account, or Esc to cancel.')) {
2719
2658
  await login();
2720
2659
  auth = describeSetupAuthState();
2721
2660
  }
2722
2661
  else {
2723
2662
  throw new Error('Setup cancelled.');
2724
2663
  }
2664
+ if (auth.state === 'signed-in' && !validateOnly && !setupAuthContext) {
2665
+ setupAuthContext = captureAuthenticatedRequestContext({ allowRefresh: !dryRun });
2666
+ }
2667
+ if (setupAuthContext)
2668
+ auth = { state: 'signed-in', baseUrl: setupAuthContext.baseUrl };
2725
2669
  const serverCreateRequested = createTeamArg !== null || createProjectArg !== null;
2726
2670
  if ((noInput || serverCreateRequested) && auth.state === 'signed-out' && !dryRun && !validateOnly && !fix) {
2727
2671
  throw new Error('Non-interactive setup requires authentication. Run `orizu login --headless`, then retry `orizu setup`.');
@@ -2731,17 +2675,18 @@ async function setupCommand() {
2731
2675
  let selectedProject = null;
2732
2676
  let setupProjects = [];
2733
2677
  if (auth.state === 'signed-in' && !validateOnly) {
2734
- setupTeam = await resolveSetupTeam(teamArg, createTeamArg, noInput, dryRun, workspaceTeamSlug);
2678
+ const setupRequest = setupAuthContext.fetch.bind(setupAuthContext);
2679
+ setupTeam = await resolveSetupTeam(teamArg, createTeamArg, noInput, dryRun, workspaceTeamSlug, setupRequest);
2735
2680
  if (workspaceTeamSlug && setupTeam.slug !== workspaceTeamSlug) {
2736
2681
  throw new Error(`This directory is already an Orizu workspace for team '${workspaceTeamSlug}'. Run setup in another directory to set up team '${setupTeam.slug}'.`);
2737
2682
  }
2738
2683
  if (fix) {
2739
- setupProjects = await fetchProjects(setupTeam.slug);
2684
+ setupProjects = await fetchProjects(setupTeam.slug, setupRequest);
2740
2685
  if (setupProjects.length === 0)
2741
2686
  throw new Error(`Team '${setupTeam.slug}' has no projects to restore.`);
2742
2687
  }
2743
2688
  else {
2744
- const selection = await resolveSetupProjects(setupTeam, projectArg, createProjectArg, noInput, dryRun);
2689
+ const selection = await resolveSetupProjects(setupTeam, projectArg, createProjectArg, noInput, dryRun, setupRequest);
2745
2690
  selectedProject = selection.selected;
2746
2691
  setupProjects = selection.projects;
2747
2692
  printLine(`Selected project ${sanitizeTerminalText(`${setupTeam.slug}/${selectedProject.slug}`)}`);
@@ -2754,7 +2699,7 @@ async function setupCommand() {
2754
2699
  let workspaceResult = null;
2755
2700
  let validationLogPath = null;
2756
2701
  if (hasArg('--no-workspace') && !validateOnly && !fix) {
2757
- printLine(' Skipped (--no-workspace).');
2702
+ printLine('Skipped (--no-workspace).');
2758
2703
  }
2759
2704
  else {
2760
2705
  const wantsWorkspace = validateOnly
@@ -2772,20 +2717,20 @@ async function setupCommand() {
2772
2717
  teamSlug = setupTeam.slug;
2773
2718
  teamId = setupTeam.id;
2774
2719
  projects = projectSeedsFromProjects(setupProjects);
2775
- printLine(` ${formatProjectSetupProgress(projects.length)}`);
2720
+ printLine(formatProjectSetupProgress(projects.length));
2776
2721
  }
2777
2722
  else if (!teamSlug && !noInput && !validateOnly) {
2778
- teamSlug = await askText(' Team slug?', 'local-team');
2723
+ teamSlug = await askText('Team slug?', 'local-team');
2779
2724
  }
2780
2725
  if (auth.state !== 'signed-in' && (!teamSlug || !projectSlug) && noInput && hasArg('--workspace')) {
2781
- printLine(' Using starter local-only team/project. Pass --team with authentication to materialize server projects.');
2726
+ printLine('Using starter local-only team/project. Pass --team with authentication to materialize server projects.');
2782
2727
  }
2783
2728
  if (projectSlug && auth.state !== 'signed-in') {
2784
2729
  projects = [{ slug: projectSlug }];
2785
2730
  }
2786
2731
  if (auth.state !== 'signed-in' && !validateOnly) {
2787
2732
  const localProjectCount = projects?.length || 1;
2788
- printLine(` ${formatProjectSetupProgress(localProjectCount)}`);
2733
+ printLine(formatProjectSetupProgress(localProjectCount));
2789
2734
  }
2790
2735
  const result = initOrizuWorkspace({
2791
2736
  workspaceRoot,
@@ -2806,14 +2751,14 @@ async function setupCommand() {
2806
2751
  workspaceState = result.state;
2807
2752
  if (dryRun || verbose) {
2808
2753
  for (const action of result.actions) {
2809
- printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`);
2754
+ printLine(`${dryRun ? 'Would ' : 'Did '}${action}`);
2810
2755
  }
2811
2756
  }
2812
2757
  else if (result.actions.length > 0) {
2813
- printLine(validateOnly ? ' Workspace validation complete' : ' Workspace setup complete');
2758
+ printLine(validateOnly ? 'Workspace validation complete' : 'Workspace setup complete');
2814
2759
  }
2815
2760
  else if (!validateOnly) {
2816
- printLine(' Workspace setup complete');
2761
+ printLine('Workspace setup complete');
2817
2762
  }
2818
2763
  validationLogPath = writeWorkspaceValidationLog(result.root, result.findings, dryRun);
2819
2764
  if (verbose) {
@@ -2824,7 +2769,7 @@ async function setupCommand() {
2824
2769
  }
2825
2770
  }
2826
2771
  else {
2827
- printLine(` Skipped. Rerun with --workspace to initialize the workspace contract.`);
2772
+ printLine('Skipped. Rerun with --workspace to initialize the workspace contract.');
2828
2773
  }
2829
2774
  }
2830
2775
  printLine('');
@@ -2832,28 +2777,21 @@ async function setupCommand() {
2832
2777
  let installOutcomes = [];
2833
2778
  printLine('Step 3: Install coding agent skills');
2834
2779
  if (hasArg('--no-install') || validateOnly || fix) {
2835
- printLine(hasArg('--no-install') ? ' Skipped (--no-install).' : ' Skipped for validation/repair.');
2780
+ printLine(hasArg('--no-install') ? 'Skipped (--no-install).' : 'Skipped for validation/repair.');
2836
2781
  }
2837
2782
  else {
2838
2783
  let targets = setupSkillTargetsFromArgs();
2839
2784
  if (targets.length === 0 && !noInput && !dryRun) {
2840
- if (await askYesNo(' Install the Orizu skill for your coding agents?', true)) {
2841
- targets = ['agent-user'];
2842
- const choices = detectedSetupSkillChoices(setupSkillHome);
2843
- if (choices.length > 0) {
2844
- const selectedChoices = await promptKeyboardMultiSelect('Choose additional native agent installs', choices, choice => choice.label, () => true);
2845
- for (const choice of selectedChoices) {
2846
- if (!targets.includes(choice.target))
2847
- targets.push(choice.target);
2848
- }
2849
- }
2785
+ if (await askYesNo('Install the Orizu skill for your coding agents?', true)) {
2786
+ const selectedChoices = await promptKeyboardMultiSelect('Choose skill installation destinations', detectedSetupSkillChoices(setupSkillHome), choice => choice.label, () => true);
2787
+ targets = selectedChoices.map(choice => choice.target);
2850
2788
  }
2851
2789
  }
2852
2790
  if (targets.length === 0) {
2853
- printLine(' Skipped skill install.');
2791
+ printLine('Skipped skill install.');
2854
2792
  }
2855
2793
  else {
2856
- printLine(' Installing skills...');
2794
+ printLine('Installing skills...');
2857
2795
  installOutcomes = await applySkillInstallTargets(targets, {
2858
2796
  mode: installMode,
2859
2797
  skipConfirm: true,
@@ -2864,18 +2802,18 @@ async function setupCommand() {
2864
2802
  printResults: false,
2865
2803
  });
2866
2804
  if (dryRun) {
2867
- printLine(' Dry run: no skill files were changed.');
2805
+ printLine('Dry run: no skill files were changed.');
2868
2806
  }
2869
2807
  for (const outcome of installOutcomes) {
2870
2808
  const label = setupSkillLabel(outcome.target);
2871
2809
  if (outcome.action === 'failed') {
2872
- printLine(` ${label} failed: ${outcome.error}`);
2810
+ printLine(`${label} failed: ${sanitizeSetupHumanInlineText(outcome.error)}`);
2873
2811
  }
2874
2812
  else if (outcome.action === 'skipped') {
2875
- printLine(` ${label} skipped`);
2813
+ printLine(`${label} skipped`);
2876
2814
  }
2877
2815
  else {
2878
- printLine(` ${label} installed`);
2816
+ printLine(`${label} installed`);
2879
2817
  }
2880
2818
  }
2881
2819
  }
@@ -2894,51 +2832,64 @@ async function setupCommand() {
2894
2832
  printLine('--- prompt end ---');
2895
2833
  printLine('');
2896
2834
  }
2897
- const detected = Object.keys(LAUNCHABLE_AGENTS)
2898
- .filter(agent => findExecutable(LAUNCHABLE_AGENTS[agent]));
2835
+ const detected = Object.entries(AGENT_LAUNCH_SPECS)
2836
+ .filter(([, spec]) => findExecutable(spec.command))
2837
+ .map(([agent]) => agent);
2899
2838
  let launchAgent = getArg('--launch');
2900
- if (launchAgent && !(launchAgent in LAUNCHABLE_AGENTS)) {
2901
- throw new Error(`Unknown --launch agent '${launchAgent}'. Choices: ${Object.keys(LAUNCHABLE_AGENTS).join(', ')}.`);
2839
+ let completedInteractiveHandoff = false;
2840
+ if (launchAgent && !(launchAgent in AGENT_LAUNCH_SPECS)) {
2841
+ throw new Error(`Unknown --launch agent '${launchAgent}'. Choices: ${Object.keys(AGENT_LAUNCH_SPECS).join(', ')}.`);
2902
2842
  }
2903
- if (!launchAgent && !noInput && isInteractiveTerminal() && detected.length > 0 && !dryRun) {
2904
- const launchChoice = await promptKeyboardSelect('Choose an agent to launch', [null, ...detected], agent => agent === null ? "Don't launch an agent" : LAUNCHABLE_AGENTS[agent]);
2905
- launchAgent = launchChoice;
2843
+ if (!launchAgent && !noInput && isInteractiveTerminal() && !dryRun) {
2844
+ const launchChoice = await promptKeyboardSelect('Choose an agent to launch', [...detected, 'other'], agent => agent === 'other' ? 'Other' : AGENT_LAUNCH_SPECS[agent].displayLabel, { onEscape: () => 'escape', escapeLabel: 'skip' });
2845
+ completedInteractiveHandoff = true;
2846
+ if (launchChoice === 'other') {
2847
+ if (!handoffRequested) {
2848
+ printLine('--- prompt start ---');
2849
+ printLine(handoffPrompt);
2850
+ printLine('--- prompt end ---');
2851
+ printLine('');
2852
+ }
2853
+ }
2854
+ else if (launchChoice !== 'escape') {
2855
+ launchAgent = launchChoice;
2856
+ }
2906
2857
  }
2907
2858
  if (launchAgent) {
2908
- const binary = findExecutable(LAUNCHABLE_AGENTS[launchAgent]);
2859
+ const launchSpec = AGENT_LAUNCH_SPECS[launchAgent];
2860
+ const binary = findExecutable(launchSpec.command);
2909
2861
  if (inputDisabled) {
2910
- printLine(' Not launching: setup is non-interactive.');
2862
+ printLine('Not launching: setup is non-interactive.');
2911
2863
  }
2912
2864
  else if (!isInteractiveTerminal()) {
2913
- printLine(' Not launching: --launch requires an interactive terminal.');
2865
+ printLine('Not launching: --launch requires an interactive terminal.');
2914
2866
  }
2915
2867
  else if (!binary) {
2916
- printLine(` Not launching: '${LAUNCHABLE_AGENTS[launchAgent]}' was not found on PATH.`);
2868
+ printLine(`Not launching: '${launchSpec.command}' was not found on PATH.`);
2917
2869
  }
2918
2870
  else if (dryRun) {
2919
- printLine(` Would launch ${binary} with the setup prompt.`);
2871
+ printLine(`Would launch ${binary} with the setup prompt.`);
2920
2872
  }
2921
- else if (!getArg('--launch') || skipConfirm || await askYesNo(` Launch ${LAUNCHABLE_AGENTS[launchAgent]} with the setup prompt now?`, true)) {
2922
- printLine(` Launching ${binary}…`);
2923
- const launched = spawnSync(binary, [handoffPrompt], { cwd: workspaceRoot, stdio: 'inherit' });
2873
+ else if (!getArg('--launch') || skipConfirm || await askYesNo(`Launch ${launchSpec.displayLabel} with the setup prompt now?`, true)) {
2874
+ printLine(`Launching ${binary}…`);
2875
+ const launched = spawnSync(binary, launchSpec.buildPromptArgs(handoffPrompt), {
2876
+ cwd: workspaceRoot,
2877
+ stdio: 'inherit',
2878
+ shell: false,
2879
+ });
2924
2880
  if (!launched.error)
2925
2881
  launchedAgent = launchAgent;
2926
2882
  if (launched.error || (launched.status !== null && launched.status !== 0)) {
2927
- printLine(` ${LAUNCHABLE_AGENTS[launchAgent]} exited without completing successfully.`);
2883
+ printLine(`${launchSpec.displayLabel} exited without completing successfully.`);
2928
2884
  }
2929
2885
  }
2930
2886
  }
2931
- else if (detected.length === 0) {
2932
- printLine(` No launchable coding agent was found. Read https://orizu.ai/llms.txt to get started.`);
2887
+ else if (detected.length === 0 && !completedInteractiveHandoff) {
2888
+ printLine('No launchable coding agent was found. Read https://orizu.ai/llms.txt to get started.');
2933
2889
  }
2934
2890
  printLine('');
2935
2891
  }
2936
2892
  // Setup summary (success criteria checklist)
2937
- const summaryTargets = installOutcomes.length > 0
2938
- ? installOutcomes
2939
- .filter(outcome => outcome.action !== 'failed')
2940
- .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target, { cwd: workspaceRoot, homeDir: setupSkillHome }) }))
2941
- : [];
2942
2893
  const source = resolveSkillSource();
2943
2894
  if (hasJsonFlag()) {
2944
2895
  printJson({
@@ -2984,19 +2935,8 @@ async function setupCommand() {
2984
2935
  }
2985
2936
  printLine('Setup summary');
2986
2937
  printLine(` Auth: ${auth.state === 'signed-in' ? `signed in (${sanitizeTerminalText(auth.baseUrl)})` : 'not signed in — run `orizu login`'}`);
2987
- if (summaryTargets.length > 0) {
2988
- printLine(' Skills:');
2989
- for (const { outcome, status } of summaryTargets) {
2990
- const modeLabel = status.mode ? `, ${status.mode}` : '';
2991
- printLine(` ${outcome.target.padEnd(16)} ${status.state}${modeLabel} ${outcome.path}`);
2992
- }
2993
- }
2994
- else {
2995
- printLine(' Skills: none installed this run');
2996
- }
2997
- const failed = installOutcomes.filter(outcome => outcome.action === 'failed');
2998
- for (const failure of failed) {
2999
- printLine(` ${failure.target.padEnd(16)} failed: ${failure.error}`);
2938
+ for (const line of renderSetupSkillSummary(installOutcomes, setupSkillHome)) {
2939
+ printLine(line);
3000
2940
  }
3001
2941
  printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : getWorkspaceRoot(workspaceRoot)}`);
3002
2942
  if (workspaceResult) {