@dharma-ai-labs/agent-fabric 0.1.13 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { execFile } from 'node:child_process';
3
3
  import { createHash, createPrivateKey, createPublicKey, randomUUID } from 'node:crypto';
4
4
  import { realpathSync } from 'node:fs';
5
- import { access, link, mkdir, open, readFile, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
5
+ import { access, link, mkdir, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
6
6
  import { homedir } from 'node:os';
7
7
  import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
@@ -12,34 +12,40 @@ import { buildTrajectoryCapsule, redactValue, referencesExcludedPath } from '@dh
12
12
  import { assertPolicy, loadOrganizationPolicy, verifyServerAuthorizedPolicy } from '@dharma-ai-labs/agent-fabric-policy';
13
13
  import { agyAdapter, claudeAdapter, codexAdapter, providerAdapters } from '@dharma-ai-labs/agent-fabric-provider-adapters';
14
14
  import { AgentFabricClient, beginEnrollment, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, saveDeviceConfig, } from '@dharma-ai-labs/agent-fabric-relay-client';
15
+ import { AgentFabricClient as AgentFabricApiClient } from '@dharma-ai-labs/agent-fabric-sdk';
15
16
  import { getActiveSkillBundleId, installSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
16
17
  import { executeTask, FileTaskReceiptStore } from '@dharma-ai-labs/agent-fabric-task-runner';
17
- const VERSION = '0.1.13';
18
- const USAGE = 'Usage: dharma <onboard|login|status|providers list|workspace add|workspace sync|evidence preview|evidence capture|evidence capture-batch|evidence sync|evidence run-request|relay start|tasks run-once|skills sync|skills status|skills verify> [options]';
18
+ import { CLI_USAGE } from './usage.js';
19
+ const VERSION = '0.1.19';
20
+ const USAGE = CLI_USAGE;
19
21
  const execFileAsync = promisify(execFile);
20
- function options(args) {
22
+ export function parseCliOptions(args) {
21
23
  const positional = [];
22
24
  const flags = new Map();
25
+ const repeated = new Map();
23
26
  for (let index = 0; index < args.length; index += 1) {
24
27
  const value = args[index];
25
28
  if (!value.startsWith('--')) {
26
29
  positional.push(value);
27
30
  continue;
28
31
  }
29
- const [rawKey, inline] = value.slice(2).split('=', 2);
32
+ const rawOption = value.slice(2);
33
+ const separator = rawOption.indexOf('=');
34
+ const rawKey = separator < 0 ? rawOption : rawOption.slice(0, separator);
35
+ const inline = separator < 0 ? undefined : rawOption.slice(separator + 1);
30
36
  if (inline !== undefined) {
31
37
  flags.set(rawKey, inline);
38
+ repeated.set(rawKey, [...(repeated.get(rawKey) || []), inline]);
32
39
  continue;
33
40
  }
34
41
  const next = args[index + 1];
35
- if (next && !next.startsWith('--')) {
36
- flags.set(rawKey, next);
42
+ const parsed = next && !next.startsWith('--') ? next : true;
43
+ flags.set(rawKey, parsed);
44
+ repeated.set(rawKey, [...(repeated.get(rawKey) || []), parsed]);
45
+ if (parsed !== true)
37
46
  index += 1;
38
- }
39
- else
40
- flags.set(rawKey, true);
41
47
  }
42
- return { positional, flags };
48
+ return { positional, flags, repeated };
43
49
  }
44
50
  function required(flags, name) {
45
51
  const value = flags.get(name);
@@ -47,6 +53,9 @@ function required(flags, name) {
47
53
  throw new Error(`Missing required option --${name}.`);
48
54
  return value;
49
55
  }
56
+ export function portalUrl(flags, fallback = 'https://www.dharma-ai.io') {
57
+ return String(flags.get('portal-url') || flags.get('hq-url') || fallback);
58
+ }
50
59
  function print(value) {
51
60
  process.stdout.write(typeof value === 'string' ? `${value}\n` : `${JSON.stringify(value, null, 2)}\n`);
52
61
  }
@@ -97,6 +106,33 @@ function assertEvidenceLedger(ledger, day) {
97
106
  throw new Error('Evidence upload ledger is invalid.');
98
107
  }
99
108
  }
109
+ function evidenceLedgerForPolicyActivation(value, day) {
110
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
111
+ throw new Error('Evidence upload ledger is invalid.');
112
+ }
113
+ const ledger = value;
114
+ if (typeof ledger.day !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(ledger.day)) {
115
+ throw new Error('Evidence upload ledger is invalid.');
116
+ }
117
+ if (ledger.day !== day) {
118
+ if (ledger.day > day)
119
+ throw new Error('Evidence upload ledger is invalid.');
120
+ return newEvidenceUploadLedger(day);
121
+ }
122
+ if (ledger.schema === 'dharma.evidence-upload-ledger/v2') {
123
+ assertEvidenceLedger(ledger, day);
124
+ return ledger;
125
+ }
126
+ const legacyEmpty = ledger.schema === undefined
127
+ && ledger.totalBytes === 0
128
+ && Array.isArray(ledger.capsuleHashes)
129
+ && ledger.capsuleHashes.length === 0
130
+ && (ledger.capsuleBytes === undefined
131
+ || (typeof ledger.capsuleBytes === 'object' && Object.keys(ledger.capsuleBytes).length === 0));
132
+ if (!legacyEmpty)
133
+ throw new Error('Evidence upload ledger is invalid.');
134
+ return newEvidenceUploadLedger(day);
135
+ }
100
136
  async function pathExists(path) {
101
137
  try {
102
138
  await access(path);
@@ -690,8 +726,11 @@ async function applyWorkspaceAuthorizationAtomically(input) {
690
726
  const contentAuthorized = input.policy.evidence.automaticDisclosure?.mode === 'customer_authorized_content';
691
727
  const ledgerExists = await pathExists(evidenceUploadLedgerPath());
692
728
  if (contentAuthorized && ledgerExists) {
693
- const ledger = JSON.parse(await readFile(evidenceUploadLedgerPath(), 'utf8'));
694
- assertEvidenceLedger(ledger, ledger.day);
729
+ const current = JSON.parse(await readFile(evidenceUploadLedgerPath(), 'utf8'));
730
+ const ledger = evidenceLedgerForPolicyActivation(current, new Date().toISOString().slice(0, 10));
731
+ if (JSON.stringify(current) !== JSON.stringify(ledger)) {
732
+ await writeJsonAtomic(evidenceUploadLedgerPath(), ledger);
733
+ }
695
734
  }
696
735
  if (contentAuthorized && !ledgerExists) {
697
736
  const ledger = newEvidenceUploadLedger(new Date().toISOString().slice(0, 10));
@@ -808,6 +847,62 @@ function deterministicUuid(value) {
808
847
  const hex = bytes.toString('hex');
809
848
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
810
849
  }
850
+ export function normalizeGitRemoteIdentity(value) {
851
+ const raw = value.trim();
852
+ if (!raw || /[\u0000-\u001f\u007f]/.test(raw))
853
+ throw new Error('Git remote is empty or invalid.');
854
+ let host;
855
+ let repositoryPath;
856
+ const scp = raw.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
857
+ if (!raw.includes('://') && scp && !/^[A-Za-z]:[\\/]/.test(raw)) {
858
+ host = scp[1].toLowerCase();
859
+ repositoryPath = scp[2];
860
+ }
861
+ else {
862
+ let remote;
863
+ try {
864
+ remote = new URL(raw);
865
+ }
866
+ catch {
867
+ throw new Error('Git remote must be a hosted URL or use --repository-key.');
868
+ }
869
+ if (!['https:', 'http:', 'ssh:', 'git:'].includes(remote.protocol) || !remote.hostname) {
870
+ throw new Error('Local and file Git remotes require an explicit stable --repository-key.');
871
+ }
872
+ host = remote.hostname.toLowerCase();
873
+ const port = remote.port && !((remote.protocol === 'https:' && remote.port === '443') || (remote.protocol === 'http:' && remote.port === '80'))
874
+ ? `:${remote.port}` : '';
875
+ host += port;
876
+ repositoryPath = remote.pathname;
877
+ }
878
+ repositoryPath = repositoryPath.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
879
+ if (!repositoryPath || repositoryPath.includes('..') || /\s/.test(repositoryPath)) {
880
+ throw new Error('Git remote repository path is invalid.');
881
+ }
882
+ if (host === 'github.com' || host === 'gitlab.com')
883
+ repositoryPath = repositoryPath.toLowerCase();
884
+ return `${host}/${repositoryPath}`;
885
+ }
886
+ export function sourceRepositoryFingerprint(remote, explicitKey) {
887
+ const key = explicitKey?.trim();
888
+ let identity;
889
+ let source;
890
+ if (key) {
891
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/.test(key) || key.includes('..')) {
892
+ throw new Error('--repository-key must be a stable 1-160 character identifier without secrets or path traversal.');
893
+ }
894
+ identity = `key:${key}`;
895
+ source = 'explicit_key';
896
+ }
897
+ else if (remote) {
898
+ identity = `remote:${normalizeGitRemoteIdentity(remote)}`;
899
+ source = 'remote';
900
+ }
901
+ else {
902
+ throw new Error('Repository has no hosted Git remote. Supply a stable --repository-key for this repository.');
903
+ }
904
+ return { fingerprint: `sha256:${createHash('sha256').update(identity).digest('hex')}`, source };
905
+ }
811
906
  function responseTextFromEvent(value) {
812
907
  if (!value || typeof value !== 'object' || Array.isArray(value))
813
908
  return null;
@@ -901,6 +996,15 @@ async function registry() {
901
996
  return [];
902
997
  }
903
998
  }
999
+ async function saveRegistry(items) {
1000
+ await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
1001
+ await writeFile(workspaceRegistryPath(), `${JSON.stringify(items, null, 2)}\n`, { mode: 0o600 });
1002
+ }
1003
+ async function saveWorkspaceRecord(entry) {
1004
+ const items = (await registry()).filter((item) => item.workspaceId !== entry.workspaceId);
1005
+ items.push(entry);
1006
+ await saveRegistry(items);
1007
+ }
904
1008
  async function gitValue(workspace, argv) {
905
1009
  try {
906
1010
  return (await execFileAsync('git', ['-C', workspace, ...argv], { timeout: 10_000 })).stdout.trim() || null;
@@ -914,6 +1018,121 @@ async function client() {
914
1018
  await instance.openSession(VERSION);
915
1019
  return instance;
916
1020
  }
1021
+ async function organizationApi(flags) {
1022
+ let enrolled = null;
1023
+ try {
1024
+ enrolled = JSON.parse(await readFile(configPath(), 'utf8'));
1025
+ }
1026
+ catch { }
1027
+ const organizationId = String(flags.get('organization-id') || enrolled?.organizationId || '').trim();
1028
+ if (!organizationId)
1029
+ throw new Error('Organization command requires --organization-id or an enrolled device.');
1030
+ const token = String(process.env.DHARMA_ORG_API_TOKEN || '').trim();
1031
+ if (!token)
1032
+ throw new Error('Organization command requires DHARMA_ORG_API_TOKEN. Tokens are not accepted on the command line.');
1033
+ return new AgentFabricApiClient({
1034
+ organizationId,
1035
+ token,
1036
+ baseUrl: portalUrl(flags, enrolled?.hqUrl || 'https://www.dharma-ai.io'),
1037
+ });
1038
+ }
1039
+ async function commandJsonBody(flags) {
1040
+ const inline = flags.get('json-body');
1041
+ const path = flags.get('body-file');
1042
+ if (typeof inline === 'string' && typeof path === 'string') {
1043
+ throw new Error('Use either --json-body or --body-file, not both.');
1044
+ }
1045
+ const serialized = typeof inline === 'string'
1046
+ ? inline
1047
+ : typeof path === 'string'
1048
+ ? await readFile(resolve(path), 'utf8')
1049
+ : '{}';
1050
+ const parsed = JSON.parse(serialized);
1051
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1052
+ throw new Error('Command body must be a JSON object.');
1053
+ }
1054
+ return parsed;
1055
+ }
1056
+ export function requireExplicitConfirmation(flags, action) {
1057
+ if (flags.get('confirm') !== true) {
1058
+ throw new Error(`${action} requires --confirm after reviewing organization scope, cost, and authority.`);
1059
+ }
1060
+ }
1061
+ async function runOrganizationCommand(command, subcommand, flags) {
1062
+ const api = await organizationApi(flags);
1063
+ if (command === 'organization' && subcommand === 'status')
1064
+ return api.instructions();
1065
+ if (command === 'agents' && subcommand === 'list')
1066
+ return api.listAgents();
1067
+ if (command === 'agents' && subcommand === 'bind-runtime') {
1068
+ requireExplicitConfirmation(flags, 'Binding a managed or cloud BYOK runtime endpoint');
1069
+ const agentId = required(flags, 'agent-id');
1070
+ const body = await commandJsonBody(flags);
1071
+ const endpointKind = String(body.endpointKind || '');
1072
+ if (!['managed_runtime', 'cloud_byok'].includes(endpointKind)) {
1073
+ throw new Error('Runtime endpoint kind must be managed_runtime or cloud_byok.');
1074
+ }
1075
+ return api.bindRuntimeEndpoint(agentId, {
1076
+ endpointKind: endpointKind,
1077
+ managedAgentId: String(body.managedAgentId || ''),
1078
+ runtimeBindingId: String(body.runtimeBindingId || ''),
1079
+ ...(body.priority === undefined ? {} : { priority: Number(body.priority) }),
1080
+ });
1081
+ }
1082
+ if (command === 'experiments' && subcommand === 'list')
1083
+ return api.listAnalysisWindows();
1084
+ if (command === 'experiments' && subcommand === 'run') {
1085
+ requireExplicitConfirmation(flags, 'Running an experiment');
1086
+ return api.requestAnalysis(await commandJsonBody(flags));
1087
+ }
1088
+ if (command === 'failures' && subcommand === 'list')
1089
+ return api.listFailures();
1090
+ if (command === 'remediations' && subcommand === 'list')
1091
+ return api.listRemediations();
1092
+ if (command === 'remediations' && subcommand === 'act') {
1093
+ requireExplicitConfirmation(flags, 'Changing a repository remediation release');
1094
+ const targetId = required(flags, 'target-id');
1095
+ const body = await commandJsonBody(flags);
1096
+ const action = String(flags.get('action') || body.action || '');
1097
+ if (!['run_backtest', 'link_backtest', 'approve', 'merge_pr', 'release', 'expand', 'rollback'].includes(action)) {
1098
+ throw new Error('Remediation action must be run_backtest, link_backtest, approve, merge_pr, release, expand, or rollback.');
1099
+ }
1100
+ return api.transitionRemediationTarget(targetId, {
1101
+ ...body,
1102
+ action: action,
1103
+ });
1104
+ }
1105
+ if (command === 'skills' && subcommand === 'list')
1106
+ return api.listSkills();
1107
+ if (command === 'skills' && subcommand === 'release') {
1108
+ requireExplicitConfirmation(flags, 'Releasing a skill');
1109
+ return api.releaseSkill(await commandJsonBody(flags));
1110
+ }
1111
+ if (command === 'skills' && (subcommand === 'rollout' || subcommand === 'rollback')) {
1112
+ requireExplicitConfirmation(flags, `${subcommand === 'rollback' ? 'Rolling back' : 'Rolling out'} a skill`);
1113
+ const bundleId = required(flags, 'bundle-id');
1114
+ const body = await commandJsonBody(flags);
1115
+ return api.transitionSkillRollout(bundleId, {
1116
+ action: subcommand === 'rollback' ? 'rollback' : String(body.action || 'start'),
1117
+ ...(body.canaryPercent === undefined ? {} : { canaryPercent: Number(body.canaryPercent) }),
1118
+ });
1119
+ }
1120
+ if (command === 'tasks' && subcommand === 'list')
1121
+ return api.listTasks();
1122
+ if (command === 'tasks' && subcommand === 'dispatch') {
1123
+ requireExplicitConfirmation(flags, 'Dispatching a task');
1124
+ return api.dispatchTask(await commandJsonBody(flags));
1125
+ }
1126
+ if (command === 'handoffs' && subcommand === 'list')
1127
+ return api.listHandoffs();
1128
+ if (command === 'handoffs' && subcommand === 'dispatch') {
1129
+ requireExplicitConfirmation(flags, 'Dispatching an A2A handoff');
1130
+ return api.dispatchHandoff(await commandJsonBody(flags));
1131
+ }
1132
+ if (command === 'usage' && subcommand === 'list')
1133
+ return api.usage();
1134
+ return null;
1135
+ }
917
1136
  async function openVerificationUri(url) {
918
1137
  const attempts = process.platform === 'darwin'
919
1138
  ? [['open', [url]]]
@@ -939,7 +1158,7 @@ async function login(flags) {
939
1158
  pending = JSON.parse(await readFile(pendingEnrollmentPath(), 'utf8'));
940
1159
  }
941
1160
  else {
942
- const hqUrl = normalizeHqUrl(String(flags.get('hq-url') || 'https://www.dharma-ai.io'));
1161
+ const hqUrl = normalizeHqUrl(portalUrl(flags));
943
1162
  const organizationId = required(flags, 'organization-id');
944
1163
  const name = String(flags.get('device-name') || `${process.env.USER || process.env.USERNAME || 'developer'} device`);
945
1164
  const devicePlatform = await platform();
@@ -1213,19 +1432,207 @@ async function workspaceAdd(flags, positional) {
1213
1432
  const organizationId = String(flags.get('organization-id') || device.organizationId);
1214
1433
  if (organizationId !== device.organizationId)
1215
1434
  throw new Error('Workspace organization must match the enrolled device.');
1216
- await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
1217
1435
  const workspaceId = deterministicUuid(`${organizationId}:${device.deviceId}:${path}`);
1218
1436
  const remote = await gitValue(path, ['config', '--get', 'remote.origin.url']);
1437
+ const identity = sourceRepositoryFingerprint(remote, typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null);
1219
1438
  const entry = {
1220
1439
  workspaceId, organizationId, name: String(flags.get('name') || basename(path)), path,
1221
1440
  routeHash: `sha256:${createHash('sha256').update(path).digest('hex')}`,
1222
- repositoryRemoteHash: remote ? `sha256:${createHash('sha256').update(remote).digest('hex')}` : null,
1441
+ repositoryRemoteHash: identity.fingerprint,
1442
+ repositoryIdentityVersion: 'normalized-v1',
1443
+ repositoryAgentId: null,
1444
+ repositoryBindingId: null,
1445
+ repositoryAgentKey: null,
1446
+ controlBranch: null,
1223
1447
  defaultBranch: await gitValue(path, ['branch', '--show-current']), status: 'active',
1224
1448
  };
1225
- const without = (await registry()).filter((item) => item.workspaceId !== workspaceId);
1226
- without.push(entry);
1227
- await writeFile(workspaceRegistryPath(), `${JSON.stringify(without, null, 2)}\n`, { mode: 0o600 });
1228
- return { ok: true, workspaceId, organizationId, pathStoredLocally: true, pathDisclosedToServer: false };
1449
+ await saveWorkspaceRecord(entry);
1450
+ return {
1451
+ ok: true,
1452
+ workspaceId,
1453
+ organizationId,
1454
+ sourceFingerprint: identity.fingerprint,
1455
+ repositoryIdentitySource: identity.source,
1456
+ pathStoredLocally: true,
1457
+ pathDisclosedToServer: false,
1458
+ };
1459
+ }
1460
+ async function discoverRepositoryAt(path) {
1461
+ const canonical = await realpath(path);
1462
+ const topLevel = await gitValue(canonical, ['rev-parse', '--show-toplevel']);
1463
+ if (!topLevel)
1464
+ return null;
1465
+ const workspace = await realpath(topLevel);
1466
+ const remote = await gitValue(workspace, ['config', '--get', 'remote.origin.url']);
1467
+ let normalizedRemote = null;
1468
+ let fingerprint = null;
1469
+ if (remote) {
1470
+ normalizedRemote = normalizeGitRemoteIdentity(remote);
1471
+ fingerprint = sourceRepositoryFingerprint(remote).fingerprint;
1472
+ }
1473
+ return {
1474
+ name: basename(workspace),
1475
+ path: workspace,
1476
+ remote: normalizedRemote,
1477
+ sourceFingerprint: fingerprint,
1478
+ defaultBranch: await gitValue(workspace, ['branch', '--show-current']),
1479
+ connectable: Boolean(fingerprint),
1480
+ requiredAction: fingerprint ? null : 'Supply --repository-key when connecting this repository.',
1481
+ };
1482
+ }
1483
+ async function repositoriesDiscover(flags, positional, roots) {
1484
+ const requested = [...positional, ...roots.filter((value) => typeof value === 'string')];
1485
+ if (requested.length === 0)
1486
+ requested.push(String(flags.get('root') || '.'));
1487
+ const discovered = new Map();
1488
+ for (const requestedRoot of requested) {
1489
+ const root = await realpath(requestedRoot);
1490
+ const direct = await discoverRepositoryAt(root);
1491
+ if (direct) {
1492
+ discovered.set(direct.path, direct);
1493
+ continue;
1494
+ }
1495
+ for (const entry of await readdir(root, { withFileTypes: true })) {
1496
+ if (!entry.isDirectory())
1497
+ continue;
1498
+ const candidate = await discoverRepositoryAt(resolve(root, entry.name));
1499
+ if (candidate)
1500
+ discovered.set(candidate.path, candidate);
1501
+ }
1502
+ }
1503
+ return {
1504
+ ok: true,
1505
+ scanBoundary: 'explicit_roots_and_immediate_children',
1506
+ repositories: [...discovered.values()].filter(Boolean),
1507
+ };
1508
+ }
1509
+ function repositoryKeyAssignments(paths, values) {
1510
+ const keys = values.filter((value) => typeof value === 'string');
1511
+ const assignments = new Map();
1512
+ if (keys.length === 0)
1513
+ return assignments;
1514
+ const named = keys.filter((value) => value.includes('='));
1515
+ if (named.length > 0 && named.length !== keys.length) {
1516
+ throw new Error('Use either ordered --repository-key values or path=key assignments, not both.');
1517
+ }
1518
+ if (named.length > 0) {
1519
+ for (const value of named) {
1520
+ const split = value.indexOf('=');
1521
+ const path = resolve(value.slice(0, split));
1522
+ const index = paths.findIndex((candidate) => resolve(candidate) === path);
1523
+ if (index < 0)
1524
+ throw new Error(`Repository key assignment does not match a selected repository: ${value.slice(0, split)}`);
1525
+ assignments.set(index, value.slice(split + 1));
1526
+ }
1527
+ return assignments;
1528
+ }
1529
+ if (keys.length !== paths.length) {
1530
+ if (paths.length === 1 && keys.length === 1)
1531
+ return new Map([[0, keys[0]]]);
1532
+ throw new Error('Provide one ordered --repository-key per selected repository without a hosted remote.');
1533
+ }
1534
+ keys.forEach((key, index) => assignments.set(index, key));
1535
+ return assignments;
1536
+ }
1537
+ async function repositoriesConnect(flags, positional, repeatedRepos, repeatedKeys, repeatedProviders) {
1538
+ const selected = [...positional, ...repeatedRepos.filter((value) => typeof value === 'string')];
1539
+ if (selected.length === 0)
1540
+ selected.push(String(flags.get('repo') || '.'));
1541
+ const canonical = await Promise.all(selected.map((path) => realpath(path)));
1542
+ const unique = [...new Set(canonical)];
1543
+ const keys = repositoryKeyAssignments(unique, repeatedKeys.length ? repeatedKeys : (typeof flags.get('repository-key') === 'string' ? [String(flags.get('repository-key'))] : []));
1544
+ const providerIds = parseSelectedProviderIds(repeatedProviders.length ? repeatedProviders : (typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []));
1545
+ const results = [];
1546
+ for (let index = 0; index < unique.length; index += 1) {
1547
+ const connectFlags = new Map(flags);
1548
+ connectFlags.set('workspace', unique[index]);
1549
+ const repositoryKey = keys.get(index);
1550
+ if (repositoryKey)
1551
+ connectFlags.set('repository-key', repositoryKey);
1552
+ else
1553
+ connectFlags.delete('repository-key');
1554
+ if (providerIds)
1555
+ connectFlags.set('providers', providerIds.join(','));
1556
+ const result = await onboard(connectFlags);
1557
+ results.push(result);
1558
+ if (result?.stage === 'approve_device')
1559
+ break;
1560
+ }
1561
+ return { ok: true, requested: unique.length, connected: results.length, repositories: results };
1562
+ }
1563
+ export function parseSelectedProviderIds(values) {
1564
+ const selected = [...new Set(values
1565
+ .filter((value) => typeof value === 'string')
1566
+ .flatMap((value) => value.split(','))
1567
+ .map((value) => value.trim())
1568
+ .filter(Boolean))];
1569
+ if (selected.length === 0)
1570
+ return null;
1571
+ if (selected.some((value) => !['codex', 'claude', 'agy'].includes(value))) {
1572
+ throw new Error('Repository providers must be codex, claude, or agy. Repeat --provider to select more than one.');
1573
+ }
1574
+ return selected;
1575
+ }
1576
+ function selectedProviderAdapters(providerIds) {
1577
+ return providerIds
1578
+ ? providerAdapters.filter((adapter) => providerIds.includes(adapter.providerId))
1579
+ : providerAdapters;
1580
+ }
1581
+ async function repositoriesList(flags) {
1582
+ const verbose = flags.has('verbose') || flags.has('diagnostic');
1583
+ return {
1584
+ ok: true,
1585
+ repositories: (await registry()).map((item) => ({
1586
+ workspaceId: item.workspaceId,
1587
+ name: item.name,
1588
+ repositoryAgentId: item.repositoryAgentId || null,
1589
+ repositoryAgentKey: item.repositoryAgentKey || null,
1590
+ controlBranch: item.controlBranch || null,
1591
+ connected: Boolean(item.repositoryAgentId && item.controlBranch),
1592
+ ...(verbose ? { localPath: item.path, sourceFingerprint: item.repositoryRemoteHash } : {}),
1593
+ })),
1594
+ };
1595
+ }
1596
+ async function bindRepositoryAgent(fabric, item) {
1597
+ if (item.repositoryAgentId && item.repositoryBindingId && item.repositoryAgentKey && item.controlBranch)
1598
+ return item;
1599
+ const remote = await gitValue(item.path, ['config', '--get', 'remote.origin.url']);
1600
+ const currentIdentity = item.repositoryIdentityVersion === 'normalized-v1'
1601
+ ? item.repositoryRemoteHash
1602
+ : remote ? sourceRepositoryFingerprint(remote).fingerprint : item.repositoryRemoteHash;
1603
+ if (!currentIdentity) {
1604
+ throw new Error('Repository identity is missing. Re-run dharma workspace add with --repository-key.');
1605
+ }
1606
+ const response = await fabric.connectRepositoryAgent({
1607
+ sourceFingerprint: currentIdentity,
1608
+ displayName: item.name,
1609
+ defaultSourceRef: item.defaultBranch,
1610
+ workspaceId: item.workspaceId,
1611
+ legacySourceFingerprint: item.repositoryIdentityVersion === 'normalized-v1' ? null : item.repositoryRemoteHash,
1612
+ });
1613
+ const repositoryAgent = response.repositoryAgent && typeof response.repositoryAgent === 'object'
1614
+ ? response.repositoryAgent
1615
+ : null;
1616
+ const branch = repositoryAgent?.branch && typeof repositoryAgent.branch === 'object'
1617
+ ? repositoryAgent.branch
1618
+ : null;
1619
+ const updated = {
1620
+ ...item,
1621
+ repositoryRemoteHash: currentIdentity,
1622
+ repositoryIdentityVersion: 'normalized-v1',
1623
+ repositoryAgentId: String(repositoryAgent?.organization_agent_id || ''),
1624
+ repositoryBindingId: String(repositoryAgent?.id || ''),
1625
+ repositoryAgentKey: String(repositoryAgent?.agent_key || ''),
1626
+ controlBranch: String(repositoryAgent?.control_branch || branch?.branch || ''),
1627
+ };
1628
+ if (!/^[0-9a-f-]{36}$/i.test(updated.repositoryAgentId || '')
1629
+ || !/^[0-9a-f-]{36}$/i.test(updated.repositoryBindingId || '')
1630
+ || !/^repo:[a-f0-9]{24}$/.test(updated.repositoryAgentKey || '')
1631
+ || !/^agents\/[a-z0-9][a-z0-9._-]*-[a-f0-9]{8}$/.test(updated.controlBranch || '')) {
1632
+ throw new Error('Dharma HQ returned an invalid repository-agent binding.');
1633
+ }
1634
+ await saveWorkspaceRecord(updated);
1635
+ return updated;
1229
1636
  }
1230
1637
  async function workspaceSync(flags, positional) {
1231
1638
  const workspaceId = positional[0] || required(flags, 'workspace-id');
@@ -1239,13 +1646,16 @@ async function workspaceSync(flags, positional) {
1239
1646
  next: `dharma workspace sync ${workspaceId} --policy-revision ${policyRevision} --apply`,
1240
1647
  };
1241
1648
  }
1242
- return syncWorkspacePolicy(await client(), item, policyRevision, true);
1649
+ const fabric = await client();
1650
+ const providerIds = parseSelectedProviderIds(typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []);
1651
+ return syncWorkspacePolicy(fabric, await bindRepositoryAgent(fabric, item), policyRevision, true, providerIds);
1243
1652
  }
1244
- async function syncWorkspacePolicy(fabric, item, policyRevision, apply = true) {
1245
- const providers = await Promise.all(providerAdapters.map((adapter) => adapter.capability()));
1653
+ async function syncWorkspacePolicy(fabric, item, policyRevision, apply = true, providerIds = null) {
1654
+ const providers = await Promise.all(selectedProviderAdapters(providerIds).map((adapter) => adapter.capability()));
1246
1655
  const response = await fabric.registerWorkspace({
1247
1656
  workspaceId: item.workspaceId, name: item.name, routeHash: item.routeHash,
1248
1657
  repositoryRemoteHash: item.repositoryRemoteHash, defaultBranch: item.defaultBranch,
1658
+ repositoryAgentId: item.repositoryAgentId,
1249
1659
  policyRevision, providers,
1250
1660
  });
1251
1661
  const generated = await materializeWorkspacePolicy({
@@ -1334,46 +1744,68 @@ Use the installed \`dharma\` CLI for organization-scoped agent work. Never print
1334
1744
  6. Use only signed tasks whose organization, device, workspace, authority, budget, and skill pin pass local validation.
1335
1745
  7. For cross-agent help, ask the control plane for a structured, task-bound handoff. Do not open arbitrary chat, shell, file, merge, deploy, or secret authority.
1336
1746
  8. Install only signed skill bundles. Preserve the active bundle receipt and automatic rollback result.
1747
+ 9. Use the organization MCP connection for role-scoped status, experiments, failures, remediations, rollouts, and profile administration. Reads may run directly. Paid evals, task dispatch, GitHub writes, approvals, rollout, rollback, and profile mutation require the granted scope and explicit confirmation.
1337
1748
 
1338
- The organization contract and API origin are recorded in \`.dharma/agent-fabric.json\`. API calls must use the published SDK and a scoped organization token supplied at runtime, never a credential committed to this repository.
1749
+ The organization contract and API origin are recorded in \`.dharma/agent-fabric.json\`; the logical repository-agent identity is recorded in \`.dharma/repository-agent.json\`. API calls must use the published SDK and a scoped organization token supplied at runtime, never a credential committed to this repository.
1339
1750
  `;
1340
1751
  const reference = `# Organization connection
1341
1752
 
1342
1753
  - HQ API: ${input.hqUrl}
1343
1754
  - Organization: ${input.organizationId}
1344
1755
  - Workspace: ${input.workspaceId}
1756
+ - Repository agent: ${input.repositoryAgentKey || 'pending'}
1757
+ - Permanent control branch: ${input.controlBranch || 'pending'}
1345
1758
  - Policy revision: ${input.policyRevision}
1346
1759
  - OpenAPI: ${input.hqUrl}/api/v1/agent-fabric/openapi.json
1760
+ - Organization instructions: ${input.hqUrl}/api/v1/orgs/${input.organizationId}/agent-fabric/instructions
1347
1761
 
1348
1762
  The CLI enrolls this device through browser-confirmed Clerk organization consent. Local provider credentials remain on this device. Managed and cloud BYOK execution are brokered by Dharma HQ and expose neither private runtime URLs nor cloud credentials.
1349
1763
  `;
1350
1764
  const connection = {
1351
- schema: 'dharma.repository-connection/v1',
1765
+ schema: 'dharma.repository-connection/v2',
1352
1766
  hqUrl: input.hqUrl,
1353
1767
  organizationId: input.organizationId,
1354
1768
  workspaceId: input.workspaceId,
1769
+ repositoryAgentId: input.repositoryAgentId || null,
1770
+ repositoryAgentKey: input.repositoryAgentKey || null,
1771
+ controlBranch: input.controlBranch || null,
1355
1772
  policyRevision: input.policyRevision,
1356
1773
  openapiUrl: `${input.hqUrl}/api/v1/agent-fabric/openapi.json`,
1774
+ instructionsUrl: `${input.hqUrl}/api/v1/orgs/${input.organizationId}/agent-fabric/instructions`,
1775
+ };
1776
+ const repositoryAgent = {
1777
+ schema: 'dharma.repository-agent/v1',
1778
+ organizationId: input.organizationId,
1779
+ organizationAgentId: input.repositoryAgentId || null,
1780
+ agentKey: input.repositoryAgentKey || null,
1781
+ controlBranch: input.controlBranch || null,
1782
+ workspaceId: input.workspaceId,
1357
1783
  };
1358
1784
  await writeFile(resolve(skillRoot, 'SKILL.md'), skill, { mode: 0o600 });
1359
1785
  await writeFile(resolve(skillRoot, 'references', 'organization.md'), reference, { mode: 0o600 });
1360
1786
  await writeFile(marker, `${JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: input.workspaceId }, null, 2)}\n`, { mode: 0o600 });
1361
1787
  await writeFile(resolve(input.workspace, '.dharma', 'agent-fabric.json'), `${JSON.stringify(connection, null, 2)}\n`, { mode: 0o600 });
1788
+ await writeFile(resolve(input.workspace, '.dharma', 'repository-agent.json'), `${JSON.stringify(repositoryAgent, null, 2)}\n`, { mode: 0o600 });
1362
1789
  return {
1363
1790
  skillPath: '.agents/skills/dharma-agent-fabric/SKILL.md',
1364
1791
  connectionPath: '.dharma/agent-fabric.json',
1792
+ repositoryAgentPath: '.dharma/repository-agent.json',
1365
1793
  };
1366
1794
  }
1367
1795
  async function onboard(flags) {
1368
1796
  const workspace = await realpath(String(flags.get('workspace') || flags.get('path') || '.'));
1369
1797
  const organizationId = required(flags, 'organization-id');
1370
1798
  const policyRevision = required(flags, 'policy-revision');
1371
- const requestedHqUrl = normalizeHqUrl(String(flags.get('hq-url') || 'https://www.dharma-ai.io'));
1799
+ const requestedHqUrl = normalizeHqUrl(portalUrl(flags));
1372
1800
  let config = await readDeviceConfig();
1373
1801
  if (!config) {
1374
1802
  const loginFlags = new Map(flags);
1375
1803
  loginFlags.set('hq-url', requestedHqUrl);
1376
1804
  loginFlags.set('organization-id', organizationId);
1805
+ if (flags.has('non-interactive')) {
1806
+ loginFlags.set('no-browser', true);
1807
+ loginFlags.set('no-wait', true);
1808
+ }
1377
1809
  const enrollment = await login(loginFlags);
1378
1810
  if (enrollment.status !== 'approved') {
1379
1811
  return {
@@ -1390,24 +1822,30 @@ async function onboard(flags) {
1390
1822
  if (config.organizationId !== organizationId) {
1391
1823
  throw new Error('This DHARMA_HOME is enrolled to a different organization. Use a separate DHARMA_HOME for each organization.');
1392
1824
  }
1393
- if (flags.has('hq-url') && config.hqUrl !== requestedHqUrl) {
1394
- throw new Error('This device is enrolled to a different Dharma HQ origin. Use a separate DHARMA_HOME for each HQ origin.');
1825
+ if ((flags.has('portal-url') || flags.has('hq-url')) && config.hqUrl !== requestedHqUrl) {
1826
+ throw new Error('This device is enrolled to a different Dharma portal origin. Use a separate DHARMA_HOME for each portal origin.');
1395
1827
  }
1396
1828
  const hqUrl = config.hqUrl;
1829
+ const providerIds = parseSelectedProviderIds(typeof flags.get('providers') === 'string'
1830
+ ? [String(flags.get('providers'))]
1831
+ : typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []);
1397
1832
  let registered = (await registry()).find((item) => item.path === workspace);
1398
- if (!registered) {
1399
- await workspaceAdd(new Map([
1833
+ if (!registered || (!registered.repositoryRemoteHash && typeof flags.get('repository-key') === 'string')) {
1834
+ const addFlags = new Map([
1400
1835
  ['organization-id', organizationId],
1401
1836
  ['path', workspace],
1402
1837
  ['name', String(flags.get('name') || basename(workspace))],
1403
- ]), [workspace]);
1838
+ ]);
1839
+ if (typeof flags.get('repository-key') === 'string')
1840
+ addFlags.set('repository-key', String(flags.get('repository-key')));
1841
+ await workspaceAdd(addFlags, [workspace]);
1404
1842
  registered = (await registry()).find((item) => item.path === workspace);
1405
1843
  }
1406
1844
  if (!registered)
1407
1845
  throw new Error('Workspace registration failed.');
1408
- const synced = await workspaceSync(new Map([
1409
- ['policy-revision', policyRevision], ['apply', true],
1410
- ]), [registered.workspaceId]);
1846
+ const fabric = await client();
1847
+ registered = await bindRepositoryAgent(fabric, registered);
1848
+ const synced = await syncWorkspacePolicy(fabric, registered, policyRevision, true, providerIds);
1411
1849
  const localPolicy = synced.localPolicy;
1412
1850
  const authoritativeRevision = String(localPolicy?.revision || policyRevision);
1413
1851
  const generatedPolicy = await loadVerifiedWorkspacePolicy(resolve(workspace, '.dharma', 'approved-policy.json'), registered.workspaceId);
@@ -1416,9 +1854,12 @@ async function onboard(flags) {
1416
1854
  hqUrl,
1417
1855
  organizationId,
1418
1856
  workspaceId: registered.workspaceId,
1857
+ repositoryAgentId: registered.repositoryAgentId,
1858
+ repositoryAgentKey: registered.repositoryAgentKey,
1859
+ controlBranch: registered.controlBranch,
1419
1860
  policyRevision: authoritativeRevision,
1420
1861
  });
1421
- const providers = await Promise.all(providerAdapters.map((adapter) => adapter.capability()));
1862
+ const providers = await Promise.all(selectedProviderAdapters(providerIds).map((adapter) => adapter.capability()));
1422
1863
  const nativeSkillResult = await installAvailableNativeAgentFabricBootstraps({
1423
1864
  providers,
1424
1865
  workspace,
@@ -1426,11 +1867,18 @@ async function onboard(flags) {
1426
1867
  organizationId,
1427
1868
  hqUrl,
1428
1869
  });
1870
+ const primaryProvider = providers[0]?.provider || 'codex';
1429
1871
  return {
1430
1872
  ok: true,
1431
1873
  stage: nativeSkillResult.failures.length ? 'ready_with_provider_actions' : 'ready',
1432
1874
  organizationId,
1433
1875
  workspaceId: registered.workspaceId,
1876
+ repositoryAgent: {
1877
+ id: registered.repositoryAgentId,
1878
+ key: registered.repositoryAgentKey,
1879
+ bindingId: registered.repositoryBindingId,
1880
+ controlBranch: registered.controlBranch,
1881
+ },
1434
1882
  deviceId: config.deviceId,
1435
1883
  providers,
1436
1884
  organizationPolicy: {
@@ -1445,10 +1893,10 @@ async function onboard(flags) {
1445
1893
  nativeSkillFailures: nativeSkillResult.failures,
1446
1894
  workspaceSync: synced,
1447
1895
  next: {
1448
- preview: 'dharma evidence preview --workspace . --provider codex',
1449
- sync: 'dharma evidence capture-batch --workspace . --provider codex --policy .dharma/approved-policy.json --maximum-sessions 20 --sync',
1896
+ preview: `dharma evidence preview --workspace . --provider ${primaryProvider}`,
1897
+ sync: `dharma evidence capture-batch --workspace . --provider ${primaryProvider} --policy .dharma/approved-policy.json --maximum-sessions 20 --sync`,
1450
1898
  relay: 'dharma relay start --policy .dharma/approved-policy.json',
1451
- verifySkill: 'dharma skills verify --provider codex --workspace .',
1899
+ verifySkill: `dharma skills verify --provider ${primaryProvider} --workspace .`,
1452
1900
  },
1453
1901
  };
1454
1902
  }
@@ -1683,7 +2131,7 @@ async function executeOneTask(fabric, policy, leaseSeconds) {
1683
2131
  if (task.target.deviceId !== config.deviceId)
1684
2132
  throw new Error('Task target does not match this enrolled device.');
1685
2133
  const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
1686
- const activeBundleId = await getActiveSkillBundleId(nativeSkillDirectory(task.target.provider));
2134
+ const activeBundleId = await getActiveSkillBundleId(nativeSkillDirectory(task.target.provider), task.workspaceId);
1687
2135
  try {
1688
2136
  assertTaskSkillPin(task.skillBundle, activeBundleId);
1689
2137
  }
@@ -1816,6 +2264,13 @@ export async function verifyAgentFabricSkillInstallation(input) {
1816
2264
  const nativeMarkerPath = resolve(nativeRoot, 'dharma-agent-fabric', '.dharma-agent-fabric-bootstrap.json');
1817
2265
  const repositoryInstalled = await pathExists(repositorySkillPath) && await pathExists(connectionPath);
1818
2266
  const nativeInstalled = await pathExists(nativeSkillPath) && await pathExists(nativeMarkerPath);
2267
+ let workspaceId;
2268
+ try {
2269
+ const connection = JSON.parse(await readFile(connectionPath, 'utf8'));
2270
+ if (typeof connection.workspaceId === 'string')
2271
+ workspaceId = connection.workspaceId;
2272
+ }
2273
+ catch { }
1819
2274
  return {
1820
2275
  provider: input.provider,
1821
2276
  ready: repositoryInstalled && nativeInstalled,
@@ -1824,7 +2279,8 @@ export async function verifyAgentFabricSkillInstallation(input) {
1824
2279
  repositorySkillPath,
1825
2280
  connectionPath,
1826
2281
  nativeSkillPath,
1827
- activeBundleId: await getActiveSkillBundleId(nativeRoot),
2282
+ workspaceId: workspaceId || null,
2283
+ activeBundleId: workspaceId ? await getActiveSkillBundleId(nativeRoot, workspaceId) : null,
1828
2284
  activation: 'next_session',
1829
2285
  nextAction: repositoryInstalled && nativeInstalled
1830
2286
  ? `Start a new ${input.provider} session from ${workspace} and invoke the dharma-agent-fabric skill.`
@@ -1908,7 +2364,11 @@ async function skillSync(flags) {
1908
2364
  const policy = await loadOrganizationPolicy(required(flags, 'policy'));
1909
2365
  const destination = nativeSkillDirectory(provider);
1910
2366
  const fabric = await client();
1911
- const response = await fabric.pollSkill({ workspaceId, provider, installedBundleId: await getActiveSkillBundleId(destination) });
2367
+ const response = await fabric.pollSkill({
2368
+ workspaceId,
2369
+ provider,
2370
+ installedBundleId: await getActiveSkillBundleId(destination, workspaceId),
2371
+ });
1912
2372
  const rollout = response.rollout;
1913
2373
  if (!rollout)
1914
2374
  return { ok: true, rollout: null, changed: false };
@@ -2023,7 +2483,7 @@ async function relayStart(flags) {
2023
2483
  return { ok: true, stopped: true, tasksCompleted, evidenceResponsesCompleted };
2024
2484
  }
2025
2485
  export async function run(argv) {
2026
- const { positional, flags } = options(argv);
2486
+ const { positional, flags, repeated } = parseCliOptions(argv);
2027
2487
  const [command, subcommand] = positional;
2028
2488
  if (flags.has('help') || command === 'help')
2029
2489
  return USAGE;
@@ -2035,6 +2495,22 @@ export async function run(argv) {
2035
2495
  return login(flags);
2036
2496
  if (command === 'providers' && subcommand === 'list')
2037
2497
  return { providers: await Promise.all(providerAdapters.map((adapter) => adapter.capability())) };
2498
+ if (command === 'repositories' && subcommand === 'discover') {
2499
+ return repositoriesDiscover(flags, positional.slice(2), repeated.get('root') || []);
2500
+ }
2501
+ if (command === 'repositories' && subcommand === 'connect') {
2502
+ return repositoriesConnect(flags, positional.slice(2), repeated.get('repo') || [], repeated.get('repository-key') || [], repeated.get('provider') || []);
2503
+ }
2504
+ if (command === 'repositories' && subcommand === 'list')
2505
+ return repositoriesList(flags);
2506
+ if (command === 'repositories' && subcommand === 'status') {
2507
+ const listed = await repositoriesList(flags);
2508
+ return {
2509
+ ...listed,
2510
+ relay: await relayProcessState(),
2511
+ providers: await Promise.all(providerAdapters.map((adapter) => adapter.capability())),
2512
+ };
2513
+ }
2038
2514
  if (command === 'workspace' && subcommand === 'add')
2039
2515
  return workspaceAdd(flags, positional.slice(2));
2040
2516
  if (command === 'workspace' && subcommand === 'sync')
@@ -2066,12 +2542,18 @@ export async function run(argv) {
2066
2542
  return status;
2067
2543
  }
2068
2544
  }
2545
+ if ([
2546
+ 'organization', 'agents', 'experiments', 'failures', 'remediations', 'handoffs', 'usage',
2547
+ ].includes(String(command)) || (['tasks', 'skills'].includes(String(command))
2548
+ && ['list', 'dispatch', 'release', 'rollout', 'rollback'].includes(String(subcommand)))) {
2549
+ const result = await runOrganizationCommand(command, subcommand, flags);
2550
+ if (result)
2551
+ return result;
2552
+ }
2069
2553
  if (command === 'tasks' && subcommand === 'run-once')
2070
2554
  return runOneTask(flags);
2071
2555
  if (command === 'relay' && subcommand === 'start')
2072
2556
  return relayStart(flags);
2073
- if (command === 'tasks' && subcommand === 'list')
2074
- return { tasks: [], coverage: 'server_poll_requires_relay' };
2075
2557
  if (command === 'skills' && subcommand === 'sync')
2076
2558
  return skillSync(flags);
2077
2559
  if (command === 'skills' && subcommand === 'status') {
@@ -2079,7 +2561,13 @@ export async function run(argv) {
2079
2561
  if (!['codex', 'claude', 'agy'].includes(providerValue))
2080
2562
  throw new Error('Skill provider must be codex, claude, or agy.');
2081
2563
  const root = nativeSkillDirectory(providerValue);
2082
- return { provider: providerValue, activeBundleId: await getActiveSkillBundleId(root), nativeSkillDirectory: root };
2564
+ const workspaceId = required(flags, 'workspace-id');
2565
+ return {
2566
+ provider: providerValue,
2567
+ workspaceId,
2568
+ activeBundleId: await getActiveSkillBundleId(root, workspaceId),
2569
+ nativeSkillDirectory: root,
2570
+ };
2083
2571
  }
2084
2572
  if (command === 'skills' && subcommand === 'verify') {
2085
2573
  const providerValue = required(flags, 'provider');