@dharma-ai-labs/agent-fabric 0.2.119 → 0.2.121

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 (69) hide show
  1. package/AGENT_FABRIC_ONBOARDING.md +4 -0
  2. package/README.md +46 -0
  3. package/dist/demoEnrollment.d.ts.map +1 -1
  4. package/dist/demoEnrollment.js +81 -9
  5. package/dist/demoEnrollment.js.map +1 -1
  6. package/dist/demoEnrollment.test.js +185 -0
  7. package/dist/demoEnrollment.test.js.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +626 -378
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.test.js +18 -2
  12. package/dist/index.test.js.map +1 -1
  13. package/dist/legacyInstallerRecovery.d.ts +30 -0
  14. package/dist/legacyInstallerRecovery.d.ts.map +1 -0
  15. package/dist/legacyInstallerRecovery.js +107 -0
  16. package/dist/legacyInstallerRecovery.js.map +1 -0
  17. package/dist/legacyInstallerRecovery.test.d.ts +2 -0
  18. package/dist/legacyInstallerRecovery.test.d.ts.map +1 -0
  19. package/dist/legacyInstallerRecovery.test.js +126 -0
  20. package/dist/legacyInstallerRecovery.test.js.map +1 -0
  21. package/dist/relayAutostart.d.ts +1 -0
  22. package/dist/relayAutostart.d.ts.map +1 -1
  23. package/dist/relayAutostart.js +12 -3
  24. package/dist/relayAutostart.js.map +1 -1
  25. package/dist/relayAutostart.test.js +37 -0
  26. package/dist/relayAutostart.test.js.map +1 -1
  27. package/dist/relayRuntimeUpgrade.test.js +13 -2
  28. package/dist/relayRuntimeUpgrade.test.js.map +1 -1
  29. package/dist/repositoryInstallerFiles.d.ts +2 -1
  30. package/dist/repositoryInstallerFiles.d.ts.map +1 -1
  31. package/dist/repositoryInstallerFiles.js +6 -3
  32. package/dist/repositoryInstallerFiles.js.map +1 -1
  33. package/dist/repositoryOnboardingCaller.test.js +87 -1
  34. package/dist/repositoryOnboardingCaller.test.js.map +1 -1
  35. package/dist/repositoryOnboardingFlow.test.js +4 -3
  36. package/dist/repositoryOnboardingFlow.test.js.map +1 -1
  37. package/dist/repositoryRelaySupervisor.d.ts +39 -0
  38. package/dist/repositoryRelaySupervisor.d.ts.map +1 -0
  39. package/dist/repositoryRelaySupervisor.js +145 -0
  40. package/dist/repositoryRelaySupervisor.js.map +1 -0
  41. package/dist/repositoryRelaySupervisor.test.d.ts +2 -0
  42. package/dist/repositoryRelaySupervisor.test.d.ts.map +1 -0
  43. package/dist/repositoryRelaySupervisor.test.js +191 -0
  44. package/dist/repositoryRelaySupervisor.test.js.map +1 -0
  45. package/dist/skillPreparationCache.test.js +8 -4
  46. package/dist/skillPreparationCache.test.js.map +1 -1
  47. package/dist/skillPreparationCaller.test.js +4 -2
  48. package/dist/skillPreparationCaller.test.js.map +1 -1
  49. package/dist/usage.d.ts +1 -1
  50. package/dist/usage.d.ts.map +1 -1
  51. package/dist/usage.js +1 -1
  52. package/dist/usage.js.map +1 -1
  53. package/dist/workspaceRegistry.d.ts +6 -0
  54. package/dist/workspaceRegistry.d.ts.map +1 -0
  55. package/dist/workspaceRegistry.js +24 -0
  56. package/dist/workspaceRegistry.js.map +1 -0
  57. package/dist/workspaceRegistry.test.d.ts +2 -0
  58. package/dist/workspaceRegistry.test.d.ts.map +1 -0
  59. package/dist/workspaceRegistry.test.js +57 -0
  60. package/dist/workspaceRegistry.test.js.map +1 -0
  61. package/dist/workspaceRegistryRecovery.d.ts +60 -0
  62. package/dist/workspaceRegistryRecovery.d.ts.map +1 -0
  63. package/dist/workspaceRegistryRecovery.js +167 -0
  64. package/dist/workspaceRegistryRecovery.js.map +1 -0
  65. package/dist/workspaceRegistryRecovery.test.d.ts +2 -0
  66. package/dist/workspaceRegistryRecovery.test.d.ts.map +1 -0
  67. package/dist/workspaceRegistryRecovery.test.js +111 -0
  68. package/dist/workspaceRegistryRecovery.test.js.map +1 -0
  69. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,7 +17,10 @@ import { contentHash, getActiveSkillBundleAuthorization, getExpiredSkillBundleAu
17
17
  import { executeTask, FileActionExecutionJournal, FileTaskReceiptStore, providerInstructionsForTask, } from '@dharma-ai-labs/agent-fabric-task-runner';
18
18
  import { CLI_USAGE } from './usage.js';
19
19
  import { superviseRelay } from './relaySupervisor.js';
20
+ import { repositoryRelayObservationReady, runRegisteredRepositoryRelays, selectRepositoryRelayRegistrations, serializeRelayWork, waitForRelayRefresh } from './repositoryRelaySupervisor.js';
20
21
  import { disableRelayAutostart, enableRelayAutostart, inspectOwnedRelayAutostart, relayAutostartStatus, startRelayAutostart, stopRelayAutostart } from './relayAutostart.js';
22
+ import { readWorkspaceRegistry } from './workspaceRegistry.js';
23
+ import { appendRecoveredWorkspace, applyRegistryRecoveryFile, inspectRegistryRecoveryFile, resolveRegistryRecoveryProjection } from './workspaceRegistryRecovery.js';
21
24
  import { relayRuntimeObservationReady, upgradeRelayRuntime } from './relayRuntimeUpgrade.js';
22
25
  import { assertStudyExactPoll, studyTaskIdFromFlags } from './studyExactTaskSelector.js';
23
26
  import { initializeRepositoryKnowledge, readRepositoryKnowledgeSource } from './repositoryKnowledge.js';
@@ -25,6 +28,7 @@ import { inventoryRepositoryPackage, readRepositoryPackageSnapshot, readReposito
25
28
  import { validateRepositorySourceAuthorization } from './repositorySourceAuthorization.js';
26
29
  import { advanceRepositorySourceBaseline, BlockedRepositorySourceRetry, fetchRepositorySourceAuthorization, recoverPublishedLocalSourceBaseline, RepositorySourceWatcher, scanRepositorySourceChanges, seedRepositorySourceWatcher } from './repositorySourceSync.js';
27
30
  import { assertRepositoryInstallerOwnership, writeRepositoryInstallerFile } from './repositoryInstallerFiles.js';
31
+ import { recoverLegacyRepositoryInstaller, selectLegacyInstallerRecoveryWorkspace } from './legacyInstallerRecovery.js';
28
32
  import { onboardingResumeCommand, selectDeviceWorkspace, workspaceIdForDevice } from './onboardingWorkspace.js';
29
33
  import { receiveRepositoryPackageDelivery } from './repositoryPackageDelivery.js';
30
34
  import { selectInstalledRepositoryKnowledge } from './repositoryInstalledKnowledge.js';
@@ -56,7 +60,7 @@ import { openCodexAppServerTransport } from '@dharma-ai-labs/agent-fabric-provid
56
60
  import { createNamedSessionTrust, isNamedSessionOwnerReceipt, renewNamedSessionLifetime } from './namedSessionTrust.js';
57
61
  import { startNamedCodexThread } from './namedCodexThread.js';
58
62
  export { openCooperativeInboxSession } from './cooperativeInboxSession.js';
59
- const VERSION = '0.2.119';
63
+ const VERSION = '0.2.121';
60
64
  const USAGE = CLI_USAGE;
61
65
  const execFileAsync = promisify(execFile);
62
66
  const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
@@ -1294,21 +1298,28 @@ async function platform() {
1294
1298
  throw new Error(`Unsupported device platform: ${process.platform}`);
1295
1299
  }
1296
1300
  async function registry() {
1297
- try {
1298
- return JSON.parse(await readFile(workspaceRegistryPath(), 'utf8'));
1299
- }
1300
- catch {
1301
- return [];
1302
- }
1301
+ return (await readWorkspaceRegistry(workspaceRegistryPath())).records;
1303
1302
  }
1304
1303
  async function saveRegistry(items) {
1305
1304
  await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
1306
- await writeFile(workspaceRegistryPath(), `${JSON.stringify(items, null, 2)}\n`, { mode: 0o600 });
1305
+ await writeJsonAtomic(workspaceRegistryPath(), items);
1307
1306
  }
1308
1307
  async function saveWorkspaceRecord(entry) {
1309
- const items = (await registry()).filter((item) => item.workspaceId !== entry.workspaceId);
1310
- items.push(entry);
1311
- await saveRegistry(items);
1308
+ const release = await acquirePidLock(`${workspaceRegistryPath()}.mutation.lock`, 10_000, 'Workspace registry is busy; preserve it for recovery.');
1309
+ try {
1310
+ const value = await readFile(workspaceRegistryPath(), 'utf8').catch(error => {
1311
+ if (error.code === 'ENOENT')
1312
+ return '[]';
1313
+ throw error;
1314
+ });
1315
+ const items = JSON.parse(value);
1316
+ if (!Array.isArray(items))
1317
+ throw new Error('Workspace registry is invalid; preserve it for recovery.');
1318
+ await saveRegistry([...items.filter(item => item.workspaceId !== entry.workspaceId), entry]);
1319
+ }
1320
+ finally {
1321
+ await release();
1322
+ }
1312
1323
  }
1313
1324
  async function gitValue(workspace, argv) {
1314
1325
  try {
@@ -1656,6 +1667,15 @@ export async function waitForRelayReadiness(options = {}) {
1656
1667
  throw new Error(`The outbound relay did not become ready after bootstrap (${reason}).`);
1657
1668
  }
1658
1669
  async function startRelayDaemon(policyPath) {
1670
+ const config = await readDeviceConfig();
1671
+ if (!config)
1672
+ throw new Error('Relay startup requires current device enrollment.');
1673
+ const policy = await loadOrganizationPolicy(policyPath);
1674
+ const workspaceId = policy.serverAuthorization?.workspaceId;
1675
+ const selected = (await registry()).find(row => row.workspaceId === workspaceId
1676
+ && row.organizationId === config.organizationId && resolve(row.path, '.dharma', 'approved-policy.json') === policyPath);
1677
+ if (!selected)
1678
+ throw new Error('Relay policy must belong to this enrolled repository.');
1659
1679
  const alreadyRunning = await relayProcessState() === 'running';
1660
1680
  const supervisorState = await relaySupervisorProcessState();
1661
1681
  if (supervisorState === 'unknown')
@@ -1677,8 +1697,11 @@ async function startRelayDaemon(policyPath) {
1677
1697
  const binding = await readFile(resolve(dharmaHome(), 'relay', 'supervisor-workspace.json'), 'utf8')
1678
1698
  .then(value => JSON.parse(value)).catch(() => null);
1679
1699
  if (binding?.pid === pid) {
1680
- if (binding.policyPath !== policyPath) {
1681
- throw new Error('relay_workspace_conflict: the active relay supervisor belongs to another workspace.');
1700
+ if (binding.version !== VERSION || !binding.standardRepositories) {
1701
+ throw new Error('relay_runtime_upgrade_required: safely stop and upgrade the existing startup-anchor relay; do not retarget a working receiver.');
1702
+ }
1703
+ if (binding.organizationId !== config.organizationId || binding.deviceId !== config.deviceId) {
1704
+ throw new Error('relay_workspace_conflict: the active supervisor belongs to another enrollment.');
1682
1705
  }
1683
1706
  supervisorReady = true;
1684
1707
  break;
@@ -1689,7 +1712,14 @@ async function startRelayDaemon(policyPath) {
1689
1712
  }
1690
1713
  if (!supervisorReady)
1691
1714
  throw new Error('The relay supervisor did not become ready after bootstrap.');
1692
- const readiness = await waitForRelayReadiness({ expectedVersion: VERSION });
1715
+ const readiness = await waitForRelayReadiness({ expectedVersion: VERSION, attempts: 120, delayMs: 1000, processState: async () => {
1716
+ if (await relayProcessState() !== 'running')
1717
+ return 'stopped';
1718
+ const pid = Number((await readFile(resolve(dharmaHome(), 'relay', 'relay.pid'), 'utf8')).trim());
1719
+ const poll = await readFile(resolve(dharmaHome(), 'relay', 'repositories', selected.workspaceId, 'last-successful-poll.json'), 'utf8').then(value => JSON.parse(value)).catch(() => null);
1720
+ return repositoryRelayObservationReady({ observation: poll, workspaceId: selected.workspaceId,
1721
+ version: VERSION, pid }) ? 'running' : 'stopped';
1722
+ } });
1693
1723
  return { started: !alreadyRunning, supervisor: 'running', ...readiness };
1694
1724
  }
1695
1725
  async function supervisedDemoCycle(registration, signal) {
@@ -1752,7 +1782,8 @@ async function relaySupervise(flags) {
1752
1782
  const health = createDemoWatchHealthRecorder({ home: dharmaHome(), pid: process.pid, version: VERSION });
1753
1783
  try {
1754
1784
  await writeJsonAtomic(bindingPath, { pid: process.pid, workspaceId: selectedWorkspace?.workspaceId ?? null,
1755
- policyPath, demoWatches: true, version: VERSION });
1785
+ policyPath, demoWatches: true, version: VERSION, standardRepositories: Boolean(policyPath),
1786
+ organizationId: config?.organizationId ?? null, deviceId: config?.deviceId ?? null });
1756
1787
  const demo = runDemoSupervisor({ signal: controller.signal,
1757
1788
  list: () => listDemoWatchRegistrations(dharmaHome()), cycle: (registration, signal) => {
1758
1789
  if (!health.available(demoWatchRegistrationKey(registration))) {
@@ -2123,11 +2154,36 @@ async function bootstrap(flags) {
2123
2154
  const launcher = await installStableRepositoryLauncher(workspace);
2124
2155
  const autostart = flags.has('no-relay-daemon')
2125
2156
  ? { state: 'disabled', backend: null }
2126
- : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => withRelayStartupMutation(() => enableRelayAutostart({
2127
- home: dharmaHome(), workspace, policy: resolve(workspace, '.dharma', 'approved-policy.json'),
2128
- launcher: resolve(workspace, process.platform === 'win32' ? launcher.windows : launcher.shell),
2129
- version: VERSION,
2130
- })));
2157
+ : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => withRelayStartupMutation(async () => {
2158
+ const home = dharmaHome();
2159
+ const existing = await relayAutostartStatus({ home });
2160
+ if (existing.backend !== null || existing.state !== 'disabled') {
2161
+ const anchor = await inspectOwnedRelayAutostart({ home });
2162
+ if (anchor.policy !== null) {
2163
+ const current = await readDeviceConfig();
2164
+ if (!current || current.organizationId !== organizationId || current.deviceId !== config.deviceId) {
2165
+ throw new Error('relay_workspace_conflict: startup enrollment changed.');
2166
+ }
2167
+ const selected = selectDeviceWorkspace(await registry(), { organizationId,
2168
+ deviceId: config.deviceId, path: anchor.workspace });
2169
+ if (!selected)
2170
+ throw new Error('relay_workspace_conflict: startup anchor is not registered to this device.');
2171
+ if (anchor.policy !== resolve(anchor.workspace, '.dharma', 'approved-policy.json')) {
2172
+ throw new Error('relay_workspace_conflict: startup policy is not canonical.');
2173
+ }
2174
+ const authorized = await loadOrganizationPolicy(anchor.policy);
2175
+ const canonical = (await registry()).filter(row => row.workspaceId === authorized.serverAuthorization?.workspaceId
2176
+ && row.organizationId === organizationId && row.path === anchor.workspace
2177
+ && row.routeHash === selected.routeHash && row.repositoryRemoteHash === selected.repositoryRemoteHash);
2178
+ if (canonical.length !== 1)
2179
+ throw new Error('relay_workspace_conflict: startup policy route changed.');
2180
+ await loadVerifiedWorkspacePolicy(anchor.policy, authorized.serverAuthorization?.workspaceId ?? '');
2181
+ }
2182
+ }
2183
+ return enableRelayAutostart({ home, workspace, policy: resolve(workspace, '.dharma', 'approved-policy.json'),
2184
+ launcher: resolve(workspace, process.platform === 'win32' ? launcher.windows : launcher.shell),
2185
+ version: VERSION, preserveStandardAnchor: true });
2186
+ }));
2131
2187
  let sharedRepositoryReady = onboarded.sharedRepositoryReady === true;
2132
2188
  const completionRequested = flags.has('complete');
2133
2189
  if (!completionRequested) {
@@ -3006,6 +3062,65 @@ async function workspaceAdd(flags, positional) {
3006
3062
  pathDisclosedToServer: false,
3007
3063
  };
3008
3064
  }
3065
+ async function workspaceRecoverRegistry(flags) {
3066
+ if (flags.has('apply') && flags.has('dry-run'))
3067
+ throw new Error('registry_recovery_flags_conflict');
3068
+ const config = await readDeviceConfig();
3069
+ if (!config)
3070
+ throw new Error('registry_recovery_enrollment_required');
3071
+ const enrollment = await loadDeviceEnrollmentAnchor({ config });
3072
+ const workspace = await realpath(String(flags.get('workspace') || '.'));
3073
+ const home = dharmaHome();
3074
+ const startup = await inspectOwnedRelayAutostart({ home });
3075
+ const policyPath = resolve(workspace, '.dharma', 'approved-policy.json');
3076
+ const launcher = resolve(workspace, '.dharma', 'bin', process.platform === 'win32' ? 'dharma.cmd' : 'dharma');
3077
+ if (startup.workspace !== workspace || startup.policy !== policyPath || startup.launcher !== launcher) {
3078
+ throw new Error('registry_recovery_startup_anchor_mismatch');
3079
+ }
3080
+ const policy = await loadOrganizationPolicy(policyPath);
3081
+ const workspaceId = policy.serverAuthorization?.workspaceId;
3082
+ if (!workspaceId || policy.organizationId !== config.organizationId) {
3083
+ throw new Error('registry_recovery_policy_scope_mismatch');
3084
+ }
3085
+ verifyServerAuthorizedPolicy({ policy, publicKeyEd25519: enrollment.serverPublicKeyEd25519,
3086
+ organizationId: config.organizationId, workspaceId });
3087
+ await assertWorkspaceAuthorizationCurrent(workspaceId, policy.serverAuthorization);
3088
+ const remote = await gitValue(workspace, ['config', '--get', 'remote.origin.url']);
3089
+ if (!remote)
3090
+ throw new Error('registry_recovery_repository_identity_missing');
3091
+ const repositoryFingerprint = sourceRepositoryFingerprint(remote).fingerprint;
3092
+ const api = await organizationApi(new Map());
3093
+ const projection = resolveRegistryRecoveryProjection({ organizationId: config.organizationId,
3094
+ deviceId: config.deviceId, workspaceId, policyRevision: policy.revision, repositoryFingerprint,
3095
+ workspaces: await api.listWorkspaces(), repositoryAgents: await api.listRepositoryAgents() });
3096
+ const entry = {
3097
+ workspaceId, organizationId: config.organizationId,
3098
+ name: projection.name || basename(workspace), path: workspace,
3099
+ routeHash: `sha256:${createHash('sha256').update(workspace).digest('hex')}`,
3100
+ repositoryRemoteHash: repositoryFingerprint, repositoryIdentityVersion: 'normalized-v1',
3101
+ repositoryAgentId: projection.repositoryAgentId, repositoryBindingId: projection.repositoryBindingId,
3102
+ endpointId: projection.endpointId, repositoryAgentKey: projection.repositoryAgentKey,
3103
+ controlBranch: projection.controlBranch, defaultBranch: projection.defaultBranch, status: 'active',
3104
+ };
3105
+ const path = workspaceRegistryPath();
3106
+ const original = await inspectRegistryRecoveryFile(path);
3107
+ const planned = appendRecoveredWorkspace(original.records, entry);
3108
+ if (!flags.has('apply'))
3109
+ return { ok: true, dryRun: true, workspaceId, organizationId: config.organizationId,
3110
+ deviceId: config.deviceId, registryState: original.kind, registryHash: original.hash,
3111
+ backupRequired: original.bytes !== null && !planned.alreadyPresent, alreadyPresent: planned.alreadyPresent,
3112
+ serverMutation: false, newEnrollment: false, startupMutation: false, signedPackageMutation: false };
3113
+ const release = await acquirePidLock(`${path}.mutation.lock`, 10_000, 'Workspace registry is busy; preserve it for recovery.');
3114
+ try {
3115
+ const recovered = await applyRegistryRecoveryFile({ home, path,
3116
+ expectedKind: original.kind, expectedHash: original.hash, entry });
3117
+ return { ok: true, ...recovered, workspaceId, registryStateBefore: original.kind,
3118
+ serverMutation: false, newEnrollment: false, startupMutation: false, signedPackageMutation: false };
3119
+ }
3120
+ finally {
3121
+ await release();
3122
+ }
3123
+ }
3009
3124
  async function discoverRepositoryAt(path) {
3010
3125
  const canonical = await realpath(path);
3011
3126
  const topLevel = await gitValue(canonical, ['rev-parse', '--show-toplevel']);
@@ -3885,6 +4000,38 @@ The CLI enrolls this device through browser-confirmed Clerk organization consent
3885
4000
  knowledge,
3886
4001
  };
3887
4002
  }
4003
+ async function repositoryInstallerRecoveryCommand(flags) {
4004
+ if (flags.has('apply') === flags.has('dry-run'))
4005
+ throw new Error('Choose exactly one of --apply or --dry-run.');
4006
+ const fromWorkspaceId = required(flags, 'from-workspace-id');
4007
+ const workspaceId = required(flags, 'workspace-id');
4008
+ const workspace = await realpath(String(flags.get('workspace') || '.'));
4009
+ const organizationId = required(flags, 'organization-id');
4010
+ const config = await readDeviceConfig();
4011
+ if (!config || config.organizationId !== organizationId)
4012
+ throw new Error('Recovery requires current organization enrollment.');
4013
+ await loadDeviceEnrollmentAnchor({ config });
4014
+ const identity = await preflightBootstrapWorkspaceIdentity(workspace);
4015
+ const registered = selectLegacyInstallerRecoveryWorkspace(await registry(), { organizationId, deviceId: config.deviceId,
4016
+ path: workspace, repositoryRemoteHash: identity.fingerprint, workspaceId });
4017
+ const repositoryAgentKey = `repo:${identity.fingerprint.slice('sha256:'.length, 'sha256:'.length + 24)}`;
4018
+ if (!registered?.repositoryBindingId || !registered.repositoryAgentId
4019
+ || registered.repositoryAgentKey !== repositoryAgentKey) {
4020
+ throw new Error('Recovery requires the enrolled device canonical repository binding.');
4021
+ }
4022
+ const policy = await loadVerifiedWorkspacePolicy(resolve(workspace, '.dharma', 'approved-policy.json'), registered.workspaceId);
4023
+ if (policy.organizationId !== organizationId || policy.serverAuthorization?.workspaceId !== registered.workspaceId) {
4024
+ throw new Error('Recovery requires a current signed workspace policy.');
4025
+ }
4026
+ await assertWorkspaceAuthorizationCurrent(registered.workspaceId, policy.serverAuthorization);
4027
+ const scope = { organizationId, workspaceId: registered.workspaceId,
4028
+ repositoryBindingId: registered.repositoryBindingId, repositoryAgentId: registered.repositoryAgentId };
4029
+ validateRepositorySourceAuthorization(await fetchRepositorySourceAuthorization(await client(), scope), scope, new Date());
4030
+ const recovery = await recoverLegacyRepositoryInstaller({ workspace, fromWorkspaceId,
4031
+ workspaceId: registered.workspaceId, repositoryAgentKey, apply: flags.has('apply') });
4032
+ return { ok: true, dryRun: !recovery.applied, serverMutation: false, recovery,
4033
+ nextAction: 'Resume pinned onboarding; recovery is not signed skill installation or activation.' };
4034
+ }
3888
4035
  async function repositorySnapshotCommand(flags, outputs) {
3889
4036
  if (flags.has('apply') && flags.has('dry-run'))
3890
4037
  throw new Error('Choose --apply or --dry-run, not both.');
@@ -3956,6 +4103,15 @@ async function onboard(flags) {
3956
4103
  : typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []);
3957
4104
  const repositoryKey = typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null;
3958
4105
  const repositoryIdentity = await preflightBootstrapWorkspaceIdentity(workspace, repositoryKey);
4106
+ const registryState = await inspectRegistryRecoveryFile(workspaceRegistryPath());
4107
+ if (registryState.kind === 'corrupt_zero') {
4108
+ const anchor = await inspectOwnedRelayAutostart({ home: dharmaHome() });
4109
+ if (anchor.policy === null)
4110
+ throw new Error('registry_recovery_startup_anchor_mismatch');
4111
+ await workspaceRecoverRegistry(new Map([
4112
+ ['workspace', anchor.workspace], ['apply', true],
4113
+ ]));
4114
+ }
3959
4115
  let registered = selectDeviceWorkspace(await registry(), {
3960
4116
  organizationId,
3961
4117
  deviceId: config.deviceId,
@@ -5972,6 +6128,52 @@ async function takeCachedSkillUpdate(input) {
5972
6128
  }
5973
6129
  }
5974
6130
  async function relayStart(flags) {
6131
+ const anchor = resolve(required(flags, 'policy'));
6132
+ const config = await readDeviceConfig();
6133
+ if (!config)
6134
+ throw new Error('Relay requires current enrollment.');
6135
+ const declared = await loadOrganizationPolicy(anchor);
6136
+ const rows = await registry();
6137
+ const selected = rows.find(row => row.workspaceId === declared.serverAuthorization?.workspaceId
6138
+ && row.organizationId === config.organizationId && resolve(row.path, '.dharma', 'approved-policy.json') === anchor);
6139
+ if (!selected)
6140
+ throw new Error('Relay policy must be the canonical policy of one registered workspace.');
6141
+ const authorized = await selectRepositoryRelayRegistrations(rows, config, loadOrganizationPolicy);
6142
+ if (!authorized.some(row => row.workspaceId === selected.workspaceId && row.policyPath === anchor)) {
6143
+ throw new Error('Relay policy is not registered to this enrolled device.');
6144
+ }
6145
+ await loadVerifiedWorkspacePolicy(anchor, selected.workspaceId);
6146
+ const release = await acquireRelayProcessLease();
6147
+ const controller = new AbortController();
6148
+ const stop = () => controller.abort();
6149
+ process.once('SIGINT', stop);
6150
+ process.once('SIGTERM', stop);
6151
+ const serialized = serializeRelayWork();
6152
+ try {
6153
+ if (flags.has('once'))
6154
+ return await relayWorkspaceLoop(flags, controller.signal, serialized);
6155
+ await runRegisteredRepositoryRelays({ signal: controller.signal, list: async () => {
6156
+ const currentConfig = await readDeviceConfig();
6157
+ if (currentConfig?.organizationId !== config.organizationId || currentConfig.deviceId !== config.deviceId) {
6158
+ throw new Error('Relay enrollment changed.');
6159
+ }
6160
+ const records = JSON.parse(await readFile(workspaceRegistryPath(), 'utf8'));
6161
+ if (!Array.isArray(records))
6162
+ throw new Error('Invalid workspace registry.');
6163
+ return selectRepositoryRelayRegistrations(records, config, loadOrganizationPolicy);
6164
+ }, run: (row, signal) => relayWorkspaceLoop(new Map([...flags, ['policy', row.policyPath]]), signal, serialized),
6165
+ observe: event => process.stderr.write(`${JSON.stringify({ event: 'repository_relay', ...event })}\n`) });
6166
+ return { ok: true, stopped: true };
6167
+ }
6168
+ finally {
6169
+ controller.abort();
6170
+ process.removeListener('SIGINT', stop);
6171
+ process.removeListener('SIGTERM', stop);
6172
+ await release();
6173
+ }
6174
+ }
6175
+ async function relayWorkspaceLoop(flags, signal, serialized) {
6176
+ signal.throwIfAborted();
5975
6177
  const policyPath = resolve(required(flags, 'policy'));
5976
6178
  const declaredPolicy = await loadOrganizationPolicy(policyPath);
5977
6179
  const declaredWorkspaceId = declaredPolicy.serverAuthorization?.workspaceId;
@@ -5986,400 +6188,411 @@ async function relayStart(flags) {
5986
6188
  }
5987
6189
  let canonicalWorkspace = selectedWorkspace;
5988
6190
  let policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
6191
+ signal.throwIfAborted();
5989
6192
  const fabric = await client();
5990
6193
  const leaseSeconds = Number(flags.get('lease-seconds') || 120);
5991
6194
  const pollMs = Math.min(Math.max(Number(flags.get('poll-seconds') || 3), 1), 60) * 1_000;
5992
- const releaseRelayLease = await acquireRelayProcessLease();
5993
- try {
5994
- let stopping = false;
5995
- let stopPreparation;
5996
- const stop = () => { stopping = true; stopPreparation?.(); };
5997
- process.once('SIGINT', stop);
5998
- process.once('SIGTERM', stop);
5999
- let tasksCompleted = 0;
6000
- let lastProviderFailureCategory = null;
6001
- let taskTrajectoriesRecovered = 0;
6002
- let evidenceResponsesCompleted = 0;
6003
- let trajectorySyncsCompleted = 0;
6004
- let nextPolicyRefreshAt = 0;
6005
- let lastPollReceiptAt = 0;
6006
- let evidencePolicyFresh = false;
6007
- const repositorySourceWatcher = new RepositorySourceWatcher();
6008
- const blockedRepositorySourceRetry = new BlockedRepositorySourceRetry();
6009
- let repositorySourceBaselineAvailable = true;
6010
- let publishedLocalSnapshotHash = canonicalWorkspace.repositoryPackage?.publishedLocalSnapshotHash
6011
- || (canonicalWorkspace.repositoryPackage?.state === 'published'
6012
- && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
6013
- ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
6014
- if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6015
- try {
6016
- const recovered = await recoverPublishedLocalSourceBaseline({
6017
- workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
6018
- workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6019
- repositoryAgentId: canonicalWorkspace.repositoryAgentId, record: canonicalWorkspace.repositoryPackage,
6020
- });
6021
- if (recovered && recovered !== publishedLocalSnapshotHash) {
6022
- publishedLocalSnapshotHash = recovered;
6023
- canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: {
6024
- ...canonicalWorkspace.repositoryPackage, localBaselineSnapshotHash: recovered,
6025
- publishedLocalSnapshotHash: recovered,
6026
- } };
6027
- await saveWorkspaceRecord(canonicalWorkspace);
6028
- }
6029
- }
6030
- catch {
6031
- repositorySourceBaselineAvailable = false;
6195
+ let stopping = signal.aborted;
6196
+ let stopPreparation;
6197
+ const stop = () => { stopping = true; stopPreparation?.(); };
6198
+ let tasksCompleted = 0;
6199
+ let lastProviderFailureCategory = null;
6200
+ let taskTrajectoriesRecovered = 0;
6201
+ let evidenceResponsesCompleted = 0;
6202
+ let trajectorySyncsCompleted = 0;
6203
+ let nextPolicyRefreshAt = 0;
6204
+ let lastPollReceiptAt = 0;
6205
+ let evidencePolicyFresh = false;
6206
+ const repositorySourceWatcher = new RepositorySourceWatcher();
6207
+ const blockedRepositorySourceRetry = new BlockedRepositorySourceRetry();
6208
+ let repositorySourceBaselineAvailable = true;
6209
+ let publishedLocalSnapshotHash = canonicalWorkspace.repositoryPackage?.publishedLocalSnapshotHash
6210
+ || (canonicalWorkspace.repositoryPackage?.state === 'published'
6211
+ && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
6212
+ ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
6213
+ if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6214
+ try {
6215
+ const recovered = await recoverPublishedLocalSourceBaseline({
6216
+ workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
6217
+ workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6218
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId, record: canonicalWorkspace.repositoryPackage,
6219
+ });
6220
+ if (recovered && recovered !== publishedLocalSnapshotHash) {
6221
+ publishedLocalSnapshotHash = recovered;
6222
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: {
6223
+ ...canonicalWorkspace.repositoryPackage, localBaselineSnapshotHash: recovered,
6224
+ publishedLocalSnapshotHash: recovered,
6225
+ } };
6226
+ await saveWorkspaceRecord(canonicalWorkspace);
6032
6227
  }
6033
6228
  }
6034
- if (publishedLocalSnapshotHash) {
6035
- try {
6036
- await readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash);
6037
- const localHash = canonicalWorkspace.repositoryPackage?.localBaselineSnapshotHash;
6038
- if (localHash && (localHash === publishedLocalSnapshotHash
6039
- || canonicalWorkspace.repositoryPackage?.pendingLocalSnapshotHash === localHash)) {
6040
- await seedRepositorySourceWatcher({ workspace: canonicalWorkspace.path,
6041
- organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6042
- repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6043
- repositoryAgentId: canonicalWorkspace.repositoryAgentId,
6044
- publishedHash: publishedLocalSnapshotHash,
6045
- localHash,
6046
- watcher: repositorySourceWatcher });
6047
- }
6048
- }
6049
- catch {
6050
- repositorySourceBaselineAvailable = false;
6229
+ catch {
6230
+ repositorySourceBaselineAvailable = false;
6231
+ }
6232
+ }
6233
+ if (publishedLocalSnapshotHash) {
6234
+ try {
6235
+ await readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash);
6236
+ const localHash = canonicalWorkspace.repositoryPackage?.localBaselineSnapshotHash;
6237
+ if (localHash && (localHash === publishedLocalSnapshotHash
6238
+ || canonicalWorkspace.repositoryPackage?.pendingLocalSnapshotHash === localHash)) {
6239
+ await seedRepositorySourceWatcher({ workspace: canonicalWorkspace.path,
6240
+ organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6241
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6242
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId,
6243
+ publishedHash: publishedLocalSnapshotHash,
6244
+ localHash,
6245
+ watcher: repositorySourceWatcher });
6051
6246
  }
6052
6247
  }
6053
- let nextRepositorySourceScanAt = 0;
6054
- let repositorySourceCandidates = 0;
6055
- let repositorySourceFailures = 0;
6056
- let repositorySourceState = 'not_scanned';
6057
- let sourceScanFlight = null;
6058
- let sourceScanReady = null;
6059
- const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
6060
- const vault = await LocalVault.open({
6061
- root: resolve(dharmaHome(), 'vault'),
6062
- masterKey: await loadOrCreateVaultMasterKey(),
6063
- rawLocalDays: rawLocalRetentionDays(policy),
6064
- });
6065
- let skillPreparationsCompleted = 0;
6066
- let skillPreparationFailures = 0;
6067
- let skillActivationsCompleted = 0;
6068
- let skillActivationFailures = 0;
6069
- const skillActivationFailuresByProvider = {};
6070
- let nextSkillActivationAt = 0;
6071
- const skillPreparationPump = startSkillPreparationPump({
6072
- prepare: async (assertRunning) => {
6073
- await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
6074
- assertRunning();
6075
- const capability = await adapter.capability();
6076
- assertRunning();
6077
- if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6248
+ catch {
6249
+ repositorySourceBaselineAvailable = false;
6250
+ }
6251
+ }
6252
+ let nextRepositorySourceScanAt = 0;
6253
+ let repositorySourceCandidates = 0;
6254
+ let repositorySourceFailures = 0;
6255
+ let repositorySourceState = 'not_scanned';
6256
+ let sourceScanFlight = null;
6257
+ let sourceScanReady = null;
6258
+ const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
6259
+ const vault = await LocalVault.open({
6260
+ root: resolve(dharmaHome(), 'vault'),
6261
+ masterKey: await loadOrCreateVaultMasterKey(),
6262
+ rawLocalDays: rawLocalRetentionDays(policy),
6263
+ });
6264
+ if (signal.aborted)
6265
+ vault.close();
6266
+ signal.throwIfAborted();
6267
+ signal.addEventListener('abort', stop, { once: true });
6268
+ if (signal.aborted)
6269
+ stop();
6270
+ let skillPreparationsCompleted = 0;
6271
+ let skillPreparationFailures = 0;
6272
+ let skillActivationsCompleted = 0;
6273
+ let skillActivationFailures = 0;
6274
+ const skillActivationFailuresByProvider = {};
6275
+ let nextSkillActivationAt = 0;
6276
+ const skillPreparationPump = startSkillPreparationPump({
6277
+ prepare: async (assertRunning) => {
6278
+ await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
6279
+ assertRunning();
6280
+ const capability = await adapter.capability();
6281
+ assertRunning();
6282
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6283
+ return;
6284
+ const provider = adapter.providerId;
6285
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6286
+ provider, assertCurrent: assertRunning }, async () => {
6287
+ const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6288
+ provider, policyPath, fabric, automatic: true, assertRunning });
6289
+ if (!prepared) {
6290
+ assertRunning();
6078
6291
  return;
6079
- const provider = adapter.providerId;
6080
- await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6081
- provider, assertCurrent: assertRunning }, async () => {
6082
- const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6083
- provider, policyPath, fabric, automatic: true, assertRunning });
6084
- if (!prepared) {
6085
- assertRunning();
6086
- return;
6087
- }
6088
- let retained = false;
6089
- try {
6090
- prepared.assertCurrent();
6091
- const repositoryAgentId = prepared.workspace.repositoryAgentId;
6092
- if (!repositoryAgentId)
6093
- throw new Error('Preparation cache requires an enrolled repository agent.');
6094
- const pendingRecord = await serializeSkillPreparationRecord({
6095
- schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
6096
- deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
6292
+ }
6293
+ let retained = false;
6294
+ try {
6295
+ prepared.assertCurrent();
6296
+ const repositoryAgentId = prepared.workspace.repositoryAgentId;
6297
+ if (!repositoryAgentId)
6298
+ throw new Error('Preparation cache requires an enrolled repository agent.');
6299
+ const pendingRecord = await serializeSkillPreparationRecord({
6300
+ schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
6301
+ deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
6302
+ repositoryAgentId,
6303
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
6304
+ policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
6305
+ bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
6306
+ ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
6307
+ preparedAt: new Date().toISOString(), activationAuthorized: false,
6308
+ });
6309
+ prepared.assertCurrent();
6310
+ await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
6311
+ prepared.assertCurrent();
6312
+ await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6313
+ provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
6314
+ expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
6097
6315
  repositoryAgentId,
6098
- repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
6316
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
6099
6317
  policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
6100
- bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
6101
- ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
6102
- preparedAt: new Date().toISOString(), activationAuthorized: false,
6103
- });
6104
- prepared.assertCurrent();
6105
- await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
6106
- prepared.assertCurrent();
6107
- await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6108
- provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
6109
- expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
6110
- repositoryAgentId,
6111
- repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
6112
- policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
6113
- bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
6114
- onCommitted: () => { retained = true; } });
6115
- skillPreparationsCompleted += 1;
6116
- }
6117
- finally {
6118
- if (!retained)
6119
- await rm(prepared.sourceRoot, { recursive: true, force: true });
6120
- }
6121
- });
6122
- }, () => { skillPreparationFailures += 1; });
6123
- },
6124
- onError: () => { skillPreparationFailures += 1; },
6125
- });
6126
- stopPreparation = skillPreparationPump.requestStop;
6127
- if (stopping)
6128
- stopPreparation();
6129
- const applyReadySourceScan = async () => {
6130
- if (!sourceScanReady)
6131
- return;
6132
- const ready = sourceScanReady;
6133
- sourceScanReady = null;
6134
- sourceScanFlight = null;
6135
- if ('error' in ready) {
6136
- repositorySourceWatcher.invalidate();
6137
- repositorySourceFailures += 1;
6138
- repositorySourceState = 'blocked';
6139
- nextRepositorySourceScanAt = performance.now() + 60_000;
6140
- return;
6141
- }
6142
- if ('state' in ready) {
6143
- repositorySourceState = ready.state;
6144
- nextRepositorySourceScanAt = performance.now() + 15_000;
6145
- return;
6146
- }
6147
- const sourceCycle = ready.cycle;
6148
- repositorySourceState = sourceCycle.state;
6149
- if (sourceCycle.localMutation) {
6150
- repositorySourceCandidates += 1;
6151
- if (canonicalWorkspace.repositoryPackage && sourceCycle.persisted) {
6152
- canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = sourceCycle.persisted.snapshotHash;
6153
- if (sourceCycle.state === 'source_already_published') {
6318
+ bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
6319
+ onCommitted: () => { retained = true; } });
6320
+ skillPreparationsCompleted += 1;
6321
+ }
6322
+ finally {
6323
+ if (!retained)
6324
+ await rm(prepared.sourceRoot, { recursive: true, force: true });
6325
+ }
6326
+ });
6327
+ }, () => { skillPreparationFailures += 1; });
6328
+ },
6329
+ onError: () => { skillPreparationFailures += 1; },
6330
+ });
6331
+ stopPreparation = skillPreparationPump.requestStop;
6332
+ if (stopping)
6333
+ stopPreparation();
6334
+ const applyReadySourceScan = async () => {
6335
+ if (!sourceScanReady)
6336
+ return;
6337
+ const ready = sourceScanReady;
6338
+ sourceScanReady = null;
6339
+ sourceScanFlight = null;
6340
+ if ('error' in ready) {
6341
+ repositorySourceWatcher.invalidate();
6342
+ repositorySourceFailures += 1;
6343
+ repositorySourceState = 'blocked';
6344
+ nextRepositorySourceScanAt = performance.now() + 60_000;
6345
+ return;
6346
+ }
6347
+ if ('state' in ready) {
6348
+ repositorySourceState = ready.state;
6349
+ nextRepositorySourceScanAt = performance.now() + 15_000;
6350
+ return;
6351
+ }
6352
+ const sourceCycle = ready.cycle;
6353
+ repositorySourceState = sourceCycle.state;
6354
+ if (sourceCycle.localMutation) {
6355
+ repositorySourceCandidates += 1;
6356
+ if (canonicalWorkspace.repositoryPackage && sourceCycle.persisted) {
6357
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = sourceCycle.persisted.snapshotHash;
6358
+ if (sourceCycle.state === 'source_already_published') {
6359
+ canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6360
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
6361
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
6362
+ publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6363
+ }
6364
+ else if (sourceCycle.candidate) {
6365
+ const receipt = sourceCycle.candidate;
6366
+ if (typeof receipt.candidateId === 'string' && typeof receipt.operationId === 'string'
6367
+ && typeof receipt.snapshotHash === 'string' && sourceCycle.submittedManifestHash) {
6368
+ Object.assign(canonicalWorkspace.repositoryPackage, {
6369
+ state: receipt.state, candidateId: receipt.candidateId, operationId: receipt.operationId,
6370
+ snapshotHash: receipt.snapshotHash, sourceManifestHash: sourceCycle.submittedManifestHash,
6371
+ releaseId: typeof receipt.releaseId === 'string' ? receipt.releaseId : null,
6372
+ consolidationMode: 'repository_update',
6373
+ });
6374
+ }
6375
+ if (receipt.state === 'published') {
6154
6376
  canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6155
6377
  canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
6156
6378
  canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
6157
6379
  publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6158
6380
  }
6159
- else if (sourceCycle.candidate) {
6160
- const receipt = sourceCycle.candidate;
6161
- if (typeof receipt.candidateId === 'string' && typeof receipt.operationId === 'string'
6162
- && typeof receipt.snapshotHash === 'string' && sourceCycle.submittedManifestHash) {
6163
- Object.assign(canonicalWorkspace.repositoryPackage, {
6164
- state: receipt.state, candidateId: receipt.candidateId, operationId: receipt.operationId,
6165
- snapshotHash: receipt.snapshotHash, sourceManifestHash: sourceCycle.submittedManifestHash,
6166
- releaseId: typeof receipt.releaseId === 'string' ? receipt.releaseId : null,
6167
- consolidationMode: 'repository_update',
6168
- });
6169
- }
6170
- if (receipt.state === 'published') {
6171
- canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6172
- canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
6173
- canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
6174
- publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6175
- }
6176
- else if ((receipt.state === 'accepted' || receipt.state === 'processing')
6177
- && typeof receipt.operationId === 'string') {
6178
- canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6179
- canonicalWorkspace.repositoryPackage.pendingLocalOperationId = receipt.operationId;
6381
+ else if ((receipt.state === 'accepted' || receipt.state === 'processing')
6382
+ && typeof receipt.operationId === 'string') {
6383
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6384
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = receipt.operationId;
6385
+ }
6386
+ else if (receipt.state === 'blocked') {
6387
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = publishedLocalSnapshotHash;
6388
+ repositorySourceWatcher.invalidate();
6389
+ }
6390
+ }
6391
+ await saveWorkspaceRecord(canonicalWorkspace);
6392
+ }
6393
+ }
6394
+ nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
6395
+ };
6396
+ const consumeSourceScan = async () => {
6397
+ try {
6398
+ await applyReadySourceScan();
6399
+ }
6400
+ catch {
6401
+ repositorySourceWatcher.invalidate();
6402
+ repositorySourceFailures += 1;
6403
+ repositorySourceState = 'blocked';
6404
+ nextRepositorySourceScanAt = performance.now() + 60_000;
6405
+ }
6406
+ };
6407
+ try {
6408
+ signal.throwIfAborted();
6409
+ const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
6410
+ if (recoveredTaskTrajectories.state === 'completed') {
6411
+ taskTrajectoriesRecovered += recoveredTaskTrajectories.value.length;
6412
+ }
6413
+ do {
6414
+ if (stopping)
6415
+ break;
6416
+ await consumeSourceScan();
6417
+ if (Date.now() >= nextPolicyRefreshAt) {
6418
+ try {
6419
+ await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
6420
+ policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
6421
+ evidencePolicyFresh = true;
6422
+ }
6423
+ catch {
6424
+ evidencePolicyFresh = false;
6425
+ }
6426
+ nextPolicyRefreshAt = Date.now() + 60_000;
6427
+ }
6428
+ if (!sourceScanFlight && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6429
+ try {
6430
+ const current = await pollRepositoryCandidate({ transport: fabric,
6431
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6432
+ scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6433
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6434
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
6435
+ if (current && canonicalWorkspace.repositoryPackage) {
6436
+ if (current.candidateId !== canonicalWorkspace.repositoryPackage.candidateId
6437
+ || current.operationId !== canonicalWorkspace.repositoryPackage.operationId) {
6438
+ throw new Error('Repository candidate status changed identity while polling.');
6180
6439
  }
6181
- else if (receipt.state === 'blocked') {
6182
- canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = publishedLocalSnapshotHash;
6440
+ const baseline = advanceRepositorySourceBaseline(canonicalWorkspace.repositoryPackage, current);
6441
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
6442
+ state: current.state, candidateId: current.candidateId, operationId: current.operationId,
6443
+ snapshotHash: current.snapshotHash, releaseId: current.releaseId,
6444
+ ...baseline,
6445
+ generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
6446
+ publishedLocalSnapshotHash = baseline.publishedLocalSnapshotHash;
6447
+ if (current.state === 'blocked')
6183
6448
  repositorySourceWatcher.invalidate();
6184
- }
6449
+ await saveWorkspaceRecord(canonicalWorkspace);
6185
6450
  }
6186
- await saveWorkspaceRecord(canonicalWorkspace);
6451
+ }
6452
+ catch {
6453
+ repositorySourceFailures += 1;
6187
6454
  }
6188
6455
  }
6189
- nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
6190
- };
6191
- const consumeSourceScan = async () => {
6192
- try {
6193
- await applyReadySourceScan();
6194
- }
6195
- catch {
6196
- repositorySourceWatcher.invalidate();
6197
- repositorySourceFailures += 1;
6198
- repositorySourceState = 'blocked';
6199
- nextRepositorySourceScanAt = performance.now() + 60_000;
6456
+ let evidenceRequestId;
6457
+ if (evidencePolicyFresh && !stopping) {
6458
+ const evidenceCycle = await deferUnavailableRelayRetention(async () => {
6459
+ const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
6460
+ const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
6461
+ return {
6462
+ synced,
6463
+ evidenceRequestId: typeof evidence.requestId === 'string' ? evidence.requestId : undefined,
6464
+ };
6465
+ });
6466
+ if (evidenceCycle.state === 'deferred') {
6467
+ evidencePolicyFresh = false;
6468
+ }
6469
+ else {
6470
+ trajectorySyncsCompleted += evidenceCycle.value.synced;
6471
+ evidenceRequestId = evidenceCycle.value.evidenceRequestId;
6472
+ if (evidenceRequestId)
6473
+ evidenceResponsesCompleted += 1;
6474
+ }
6200
6475
  }
6201
- };
6202
- try {
6203
- const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
6204
- if (recoveredTaskTrajectories.state === 'completed') {
6205
- taskTrajectoriesRecovered += recoveredTaskTrajectories.value.length;
6476
+ const result = await serialized(async () => {
6477
+ if (stopping)
6478
+ return { taskId: null };
6479
+ return executeOneTask(fabric, leaseSeconds);
6480
+ });
6481
+ if (!stopping && Date.now() - lastPollReceiptAt >= 30_000) {
6482
+ const observation = {
6483
+ at: new Date().toISOString(), workspaceId: canonicalWorkspace.workspaceId, version: VERSION, pid: process.pid,
6484
+ };
6485
+ await writeJsonAtomic(resolve(dharmaHome(), 'relay', 'repositories', canonicalWorkspace.workspaceId, 'last-successful-poll.json'), observation);
6486
+ const binding = await readFile(resolve(dharmaHome(), 'relay', 'supervisor-workspace.json'), 'utf8')
6487
+ .then(value => JSON.parse(value)).catch(() => null);
6488
+ if (!binding || binding.workspaceId === canonicalWorkspace.workspaceId) {
6489
+ await writeJsonAtomic(resolve(dharmaHome(), 'relay', 'last-successful-poll.json'), observation);
6490
+ }
6491
+ lastPollReceiptAt = Date.now();
6206
6492
  }
6207
- do {
6208
- await consumeSourceScan();
6209
- if (Date.now() >= nextPolicyRefreshAt) {
6493
+ if (result.taskId)
6494
+ tasksCompleted += 1;
6495
+ if (typeof result.failureCategory === 'string')
6496
+ lastProviderFailureCategory = result.failureCategory;
6497
+ if (!stopping && !result.taskId && performance.now() >= nextSkillActivationAt) {
6498
+ for (const adapter of providerAdapters) {
6210
6499
  try {
6211
- await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
6212
- policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
6213
- evidencePolicyFresh = true;
6500
+ const capability = await adapter.capability();
6501
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6502
+ continue;
6503
+ const provider = adapter.providerId;
6504
+ await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
6505
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6506
+ provider, assertCurrent: () => { if (stopping)
6507
+ throw new Error('Relay is stopping.'); } }, async () => {
6508
+ const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6509
+ provider, policyPath, fabric });
6510
+ const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6511
+ provider, policyPath, fabric, automatic: true });
6512
+ if (!prepared)
6513
+ return;
6514
+ if (!cached)
6515
+ skillPreparationsCompleted += 1;
6516
+ await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
6517
+ provider, policyPath, flags: new Map() });
6518
+ skillActivationsCompleted += 1;
6519
+ });
6520
+ });
6214
6521
  }
6215
6522
  catch {
6216
- evidencePolicyFresh = false;
6523
+ skillActivationFailures += 1;
6524
+ skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
6217
6525
  }
6218
- nextPolicyRefreshAt = Date.now() + 60_000;
6219
6526
  }
6220
- if (!sourceScanFlight && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6527
+ nextSkillActivationAt = performance.now() + 60_000;
6528
+ }
6529
+ if (!stopping && !sourceScanFlight && performance.now() >= nextRepositorySourceScanAt) {
6530
+ const sourceWorkspace = canonicalWorkspace;
6531
+ const sourceBaselineHash = publishedLocalSnapshotHash;
6532
+ sourceScanFlight = (async () => {
6221
6533
  try {
6222
- const current = await pollRepositoryCandidate({ transport: fabric,
6223
- outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6224
- scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6225
- repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6226
- repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
6227
- if (current && canonicalWorkspace.repositoryPackage) {
6228
- if (current.candidateId !== canonicalWorkspace.repositoryPackage.candidateId
6229
- || current.operationId !== canonicalWorkspace.repositoryPackage.operationId) {
6230
- throw new Error('Repository candidate status changed identity while polling.');
6231
- }
6232
- const baseline = advanceRepositorySourceBaseline(canonicalWorkspace.repositoryPackage, current);
6233
- canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
6234
- state: current.state, candidateId: current.candidateId, operationId: current.operationId,
6235
- snapshotHash: current.snapshotHash, releaseId: current.releaseId,
6236
- ...baseline,
6237
- generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
6238
- publishedLocalSnapshotHash = baseline.publishedLocalSnapshotHash;
6239
- if (current.state === 'blocked')
6240
- repositorySourceWatcher.invalidate();
6241
- await saveWorkspaceRecord(canonicalWorkspace);
6534
+ if (!repositorySourceBaselineAvailable) {
6535
+ throw new Error('Repository source baseline is unavailable; refusing an unanchored update.');
6242
6536
  }
6243
- }
6244
- catch {
6245
- repositorySourceFailures += 1;
6246
- }
6247
- }
6248
- let evidenceRequestId;
6249
- if (evidencePolicyFresh) {
6250
- const evidenceCycle = await deferUnavailableRelayRetention(async () => {
6251
- const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
6252
- const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
6253
- return {
6254
- synced,
6255
- evidenceRequestId: typeof evidence.requestId === 'string' ? evidence.requestId : undefined,
6256
- };
6257
- });
6258
- if (evidenceCycle.state === 'deferred') {
6259
- evidencePolicyFresh = false;
6260
- }
6261
- else {
6262
- trajectorySyncsCompleted += evidenceCycle.value.synced;
6263
- evidenceRequestId = evidenceCycle.value.evidenceRequestId;
6264
- if (evidenceRequestId)
6265
- evidenceResponsesCompleted += 1;
6266
- }
6267
- }
6268
- const result = await executeOneTask(fabric, leaseSeconds);
6269
- if (Date.now() - lastPollReceiptAt >= 30_000) {
6270
- await writeJsonAtomic(resolve(dharmaHome(), 'relay', 'last-successful-poll.json'), {
6271
- at: new Date().toISOString(), workspaceId: canonicalWorkspace.workspaceId, version: VERSION, pid: process.pid,
6272
- });
6273
- lastPollReceiptAt = Date.now();
6274
- }
6275
- if (result.taskId)
6276
- tasksCompleted += 1;
6277
- if (typeof result.failureCategory === 'string')
6278
- lastProviderFailureCategory = result.failureCategory;
6279
- if (!result.taskId && performance.now() >= nextSkillActivationAt) {
6280
- for (const adapter of providerAdapters) {
6281
- try {
6282
- const capability = await adapter.capability();
6283
- if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6284
- continue;
6285
- const provider = adapter.providerId;
6286
- await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
6287
- await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6288
- provider, assertCurrent: () => { if (stopping)
6289
- throw new Error('Relay is stopping.'); } }, async () => {
6290
- const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6291
- provider, policyPath, fabric });
6292
- const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6293
- provider, policyPath, fabric, automatic: true });
6294
- if (!prepared)
6295
- return;
6296
- if (!cached)
6297
- skillPreparationsCompleted += 1;
6298
- await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
6299
- provider, policyPath, flags: new Map() });
6300
- skillActivationsCompleted += 1;
6301
- });
6302
- });
6537
+ if (!sourceWorkspace.repositoryBindingId || !sourceWorkspace.repositoryAgentId) {
6538
+ throw new Error('Repository source synchronization requires a complete repository binding.');
6303
6539
  }
6304
- catch {
6305
- skillActivationFailures += 1;
6306
- skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
6540
+ const installedKnowledge = await installedRepositoryKnowledge(sourceWorkspace);
6541
+ if (!installedKnowledge && (sourceWorkspace.repositoryPackage?.generation ?? 0) > 0) {
6542
+ sourceScanReady = { state: 'awaiting_signed_baseline' };
6307
6543
  }
6308
- }
6309
- nextSkillActivationAt = performance.now() + 60_000;
6310
- }
6311
- if (!sourceScanFlight && performance.now() >= nextRepositorySourceScanAt) {
6312
- const sourceWorkspace = canonicalWorkspace;
6313
- const sourceBaselineHash = publishedLocalSnapshotHash;
6314
- sourceScanFlight = (async () => {
6315
- try {
6316
- if (!repositorySourceBaselineAvailable) {
6317
- throw new Error('Repository source baseline is unavailable; refusing an unanchored update.');
6318
- }
6319
- if (!sourceWorkspace.repositoryBindingId || !sourceWorkspace.repositoryAgentId) {
6320
- throw new Error('Repository source synchronization requires a complete repository binding.');
6544
+ else {
6545
+ if (sourceWorkspace.repositoryPackage?.state === 'blocked' && sourceWorkspace.repositoryPackage.snapshotHash) {
6546
+ const blocked = await readRepositorySourceBaselineSnapshot(sourceWorkspace.path, sourceWorkspace.repositoryPackage.snapshotHash);
6547
+ if (blockedRepositorySourceRetry.consider(sourceWorkspace.repositoryPackage.candidateId ?? '', blocked.manifest.knowledge, installedKnowledge))
6548
+ repositorySourceWatcher.invalidate();
6321
6549
  }
6322
- const installedKnowledge = await installedRepositoryKnowledge(sourceWorkspace);
6323
- if (!installedKnowledge && (sourceWorkspace.repositoryPackage?.generation ?? 0) > 0) {
6324
- sourceScanReady = { state: 'awaiting_signed_baseline' };
6325
- }
6326
- else {
6327
- if (sourceWorkspace.repositoryPackage?.state === 'blocked' && sourceWorkspace.repositoryPackage.snapshotHash) {
6328
- const blocked = await readRepositorySourceBaselineSnapshot(sourceWorkspace.path, sourceWorkspace.repositoryPackage.snapshotHash);
6329
- if (blockedRepositorySourceRetry.consider(sourceWorkspace.repositoryPackage.candidateId ?? '', blocked.manifest.knowledge, installedKnowledge))
6330
- repositorySourceWatcher.invalidate();
6331
- }
6332
- const sourceCycle = await scanRepositorySourceChanges({
6333
- workspace: sourceWorkspace.path, organizationId: sourceWorkspace.organizationId,
6334
- workspaceId: sourceWorkspace.workspaceId, repositoryBindingId: sourceWorkspace.repositoryBindingId,
6335
- repositoryAgentId: sourceWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
6336
- loadRetainedKnowledge: () => installedRepositoryKnowledge(sourceWorkspace),
6337
- loadPublishedLocalBaseline: () => sourceBaselineHash
6338
- ? readRepositoryPackageSnapshot(sourceWorkspace.path, sourceBaselineHash) : Promise.resolve(null),
6339
- submitCandidate: (snapshot, expectedLatestSourceFingerprint) => synchronizeRepositoryCandidate({ transport: fabric,
6340
- outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6341
- scope: { organizationId: sourceWorkspace.organizationId,
6342
- workspaceId: sourceWorkspace.workspaceId,
6343
- repositoryBindingId: sourceWorkspace.repositoryBindingId,
6344
- repositoryAgentId: sourceWorkspace.repositoryAgentId },
6345
- snapshot, initialRepository: false, expectedLatestSourceFingerprint }),
6346
- });
6347
- sourceScanReady = { cycle: sourceCycle };
6348
- }
6349
- }
6350
- catch (error) {
6351
- sourceScanReady = { error };
6550
+ const sourceCycle = await scanRepositorySourceChanges({
6551
+ workspace: sourceWorkspace.path, organizationId: sourceWorkspace.organizationId,
6552
+ workspaceId: sourceWorkspace.workspaceId, repositoryBindingId: sourceWorkspace.repositoryBindingId,
6553
+ repositoryAgentId: sourceWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
6554
+ loadRetainedKnowledge: () => installedRepositoryKnowledge(sourceWorkspace),
6555
+ loadPublishedLocalBaseline: () => sourceBaselineHash
6556
+ ? readRepositoryPackageSnapshot(sourceWorkspace.path, sourceBaselineHash) : Promise.resolve(null),
6557
+ submitCandidate: (snapshot, expectedLatestSourceFingerprint) => synchronizeRepositoryCandidate({ transport: fabric,
6558
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6559
+ scope: { organizationId: sourceWorkspace.organizationId,
6560
+ workspaceId: sourceWorkspace.workspaceId,
6561
+ repositoryBindingId: sourceWorkspace.repositoryBindingId,
6562
+ repositoryAgentId: sourceWorkspace.repositoryAgentId },
6563
+ snapshot, initialRepository: false, expectedLatestSourceFingerprint }),
6564
+ });
6565
+ sourceScanReady = { cycle: sourceCycle };
6352
6566
  }
6353
- })();
6354
- }
6355
- if (flags.has('once'))
6356
- break;
6357
- if (!result.taskId && !evidenceRequestId)
6358
- await new Promise((accept) => setTimeout(accept, pollMs));
6359
- } while (!stopping);
6360
- }
6361
- finally {
6362
- if (sourceScanFlight)
6363
- await sourceScanFlight;
6364
- await consumeSourceScan();
6365
- await skillPreparationPump.stop();
6366
- vault.close();
6367
- process.removeListener('SIGINT', stop);
6368
- process.removeListener('SIGTERM', stop);
6369
- }
6370
- return {
6371
- ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
6372
- lastProviderFailureCategory,
6373
- evidenceResponsesCompleted, trajectorySyncsCompleted,
6374
- repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
6375
- skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
6376
- skillActivationFailuresByProvider,
6377
- sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
6378
- };
6567
+ }
6568
+ catch (error) {
6569
+ sourceScanReady = { error };
6570
+ }
6571
+ })();
6572
+ }
6573
+ if (flags.has('once'))
6574
+ break;
6575
+ if (!result.taskId && !evidenceRequestId)
6576
+ await waitForRelayRefresh(pollMs, signal);
6577
+ } while (!stopping);
6379
6578
  }
6380
6579
  finally {
6381
- await releaseRelayLease();
6580
+ if (sourceScanFlight)
6581
+ await sourceScanFlight;
6582
+ await consumeSourceScan();
6583
+ await skillPreparationPump.stop();
6584
+ vault.close();
6585
+ signal.removeEventListener('abort', stop);
6382
6586
  }
6587
+ return {
6588
+ ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
6589
+ lastProviderFailureCategory,
6590
+ evidenceResponsesCompleted, trajectorySyncsCompleted,
6591
+ repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
6592
+ skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
6593
+ skillActivationFailuresByProvider,
6594
+ sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
6595
+ };
6383
6596
  }
6384
6597
  export async function run(argv) {
6385
6598
  const { positional, flags, repeated } = parseCliOptions(argv);
@@ -6571,6 +6784,8 @@ export async function run(argv) {
6571
6784
  }
6572
6785
  if (command === 'repositories' && subcommand === 'list')
6573
6786
  return repositoriesList(flags);
6787
+ if (command === 'repositories' && subcommand === 'recover-installer')
6788
+ return repositoryInstallerRecoveryCommand(flags);
6574
6789
  if (command === 'repositories' && subcommand === 'snapshot') {
6575
6790
  return repositorySnapshotCommand(flags, repeated.get('approved-output') || []);
6576
6791
  }
@@ -6588,6 +6803,8 @@ export async function run(argv) {
6588
6803
  }
6589
6804
  if (command === 'workspace' && subcommand === 'add')
6590
6805
  return workspaceAdd(flags, positional.slice(2));
6806
+ if (command === 'workspace' && subcommand === 'recover-registry')
6807
+ return workspaceRecoverRegistry(flags);
6591
6808
  if (command === 'workspace' && subcommand === 'sync')
6592
6809
  return workspaceSync(flags, positional.slice(2));
6593
6810
  if (command === 'capture' || (command === 'evidence' && subcommand === 'capture'))
@@ -6624,6 +6841,37 @@ export async function run(argv) {
6624
6841
  try {
6625
6842
  const config = JSON.parse(await readFile(configPath(), 'utf8'));
6626
6843
  const status = { version: VERSION, enrolled: true, relay, supervisor, autostart, reconnect };
6844
+ if (flags.has('verbose') || flags.has('diagnostic')) {
6845
+ const relayPid = await readFile(resolve(dharmaHome(), 'relay', 'relay.pid'), 'utf8')
6846
+ .then(value => Number(value.trim())).catch(() => 0);
6847
+ try {
6848
+ const registryRead = await readWorkspaceRegistry(workspaceRegistryPath());
6849
+ status.workspaceRegistry = { state: registryRead.state, count: registryRead.records.length };
6850
+ if (registryRead.state === 'absent') {
6851
+ status.repositoryRelays = { state: 'unavailable', repositories: [], reason: 'workspace_registry_missing' };
6852
+ }
6853
+ else {
6854
+ const registered = await selectRepositoryRelayRegistrations(registryRead.records, config, loadOrganizationPolicy);
6855
+ const observations = [];
6856
+ for (const row of registered) {
6857
+ const observation = await readFile(resolve(dharmaHome(), 'relay', 'repositories', row.workspaceId, 'last-successful-poll.json'), 'utf8').then(value => JSON.parse(value)).catch(() => null);
6858
+ observations.push({ workspaceId: row.workspaceId,
6859
+ state: relay === 'running' && repositoryRelayObservationReady({ observation,
6860
+ workspaceId: row.workspaceId, version: VERSION, pid: relayPid }) ? 'acknowledged_recently' : 'pending',
6861
+ lastSuccessfulPollAt: typeof observation?.at === 'string' ? observation.at : null });
6862
+ }
6863
+ status.repositoryRelays = { state: 'observed', repositories: observations };
6864
+ }
6865
+ }
6866
+ catch (error) {
6867
+ const message = error instanceof Error ? error.message : '';
6868
+ const reason = ['workspace_registry_read_failed', 'workspace_registry_invalid'].includes(message)
6869
+ ? message : 'repository_relay_state_unavailable';
6870
+ if (!status.workspaceRegistry)
6871
+ status.workspaceRegistry = { state: 'unavailable', reason };
6872
+ status.repositoryRelays = { state: 'unavailable', repositories: [], reason };
6873
+ }
6874
+ }
6627
6875
  if (flags.has('verbose') || flags.has('diagnostic')) {
6628
6876
  Object.assign(status, { home: dharmaHome(), organizationId: config.organizationId, deviceId: config.deviceId });
6629
6877
  }