@notis_ai/cli 0.2.0-beta.191.1 → 0.2.0-beta.197.1

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 (107) hide show
  1. package/README.md +485 -86
  2. package/config/portal-route-roots.json +47 -0
  3. package/dist/agent-hooks/notis-agent-hook.mjs +32663 -6357
  4. package/dist/base-skills/notis-apps/SKILL.md +88 -67
  5. package/dist/base-skills/notis-apps/references/architecture.md +114 -167
  6. package/dist/base-skills/notis-apps/references/design.md +26 -107
  7. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  8. package/dist/base-skills/notis-apps/references/reading.md +52 -74
  9. package/dist/base-skills/notis-apps/references/release.md +104 -142
  10. package/dist/base-skills/notis-apps/references/sdk.md +113 -60
  11. package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
  12. package/dist/base-skills/notis-apps/references/views.md +142 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +4 -4
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
  15. package/dist/base-skills/notis-cli/references/native-databases.md +6 -6
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
  17. package/dist/base-skills/notis-query/SKILL.md +34 -60
  18. package/dist/base-skills/notis-query/references/database-discovery.md +59 -69
  19. package/dist/base-skills/notis-query/references/documents.md +63 -50
  20. package/dist/base-skills/notis-query/references/query.md +57 -524
  21. package/dist/sdk/src/bundleModule.ts +79 -0
  22. package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
  23. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  24. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  25. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  26. package/dist/sdk/src/config.ts +4 -26
  27. package/dist/sdk/src/documents.ts +2 -0
  28. package/dist/sdk/src/hooks/useAction.ts +28 -0
  29. package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
  30. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  31. package/dist/sdk/src/hooks/useHandover.ts +5 -4
  32. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  33. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  34. package/dist/sdk/src/hooks/useNotis.ts +3 -0
  35. package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
  36. package/dist/sdk/src/hooks/useQuery.ts +7 -4
  37. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  38. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  39. package/dist/sdk/src/index.ts +36 -0
  40. package/dist/sdk/src/interactions/shortcuts.tsx +17 -3
  41. package/dist/sdk/src/nativeData.ts +39 -0
  42. package/dist/sdk/src/presentation.tsx +82 -0
  43. package/dist/sdk/src/provider.tsx +9 -4
  44. package/dist/sdk/src/queryCache.ts +5 -3
  45. package/dist/sdk/src/queryErrors.ts +51 -0
  46. package/dist/sdk/src/runtime.ts +146 -8
  47. package/dist/sdk/src/space.ts +425 -0
  48. package/dist/sdk/src/styles.css +96 -72
  49. package/dist/sdk/src/vite.ts +11 -1
  50. package/dist/skill-sync/index.js +458 -68
  51. package/dist/skill-sync/index.js.map +4 -4
  52. package/dist/skill-sync-worker.mjs +451 -77
  53. package/dist/space-harness/host.js +66 -0
  54. package/dist/view-renderer/dist/host.css +1 -0
  55. package/dist/view-renderer/dist/host.js +1042 -0
  56. package/dist/view-renderer/dist/theme.css +73 -0
  57. package/dist/view-renderer/src/dom.js +180 -0
  58. package/dist/view-renderer/src/errors.js +8 -0
  59. package/dist/view-renderer/src/host.tsx +173 -0
  60. package/dist/view-renderer/src/index.js +4 -0
  61. package/dist/view-renderer/src/pool.js +76 -0
  62. package/dist/view-renderer/src/remote-broker.js +59 -0
  63. package/dist/view-renderer/src/renderer.js +201 -0
  64. package/package.json +11 -5
  65. package/skills/notis-apps/cli.md +270 -163
  66. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +3 -3
  67. package/skills/notis-onboarding/BRIEF.md +7 -3
  68. package/src/command-specs/apps.js +1 -8
  69. package/src/command-specs/files.js +29 -0
  70. package/src/command-specs/handover.js +45 -16
  71. package/src/command-specs/helpers.js +1 -0
  72. package/src/command-specs/index.js +10 -4
  73. package/src/command-specs/skills.js +229 -0
  74. package/src/command-specs/spaces.js +572 -0
  75. package/src/command-specs/views.js +69 -0
  76. package/src/runtime/agent-browser.js +30 -7
  77. package/src/runtime/app-boundary-validator.js +7 -4
  78. package/src/runtime/app-data-literals.js +82 -0
  79. package/src/runtime/app-platform.js +321 -253
  80. package/src/runtime/confined-space-build.js +74 -0
  81. package/src/runtime/confined-space-verify.js +49 -0
  82. package/src/runtime/native-record-files.js +47 -0
  83. package/src/runtime/retired-sdk-files.js +20 -0
  84. package/src/runtime/skill-sync/cloud-client.ts +26 -4
  85. package/src/runtime/skill-sync/index.ts +154 -35
  86. package/src/runtime/skill-sync/local-scanner.ts +125 -17
  87. package/src/runtime/skill-sync/native-identity.ts +102 -0
  88. package/src/runtime/skill-sync/native-plan.ts +59 -0
  89. package/src/runtime/skill-sync/sync-plan.ts +16 -9
  90. package/src/runtime/skill-sync/types.ts +32 -6
  91. package/src/runtime/source-root.js +32 -0
  92. package/src/runtime/space-collection-scaffold.js +66 -0
  93. package/src/runtime/space-database-structures.js +229 -0
  94. package/src/runtime/space-fixture-broker.js +50 -0
  95. package/src/runtime/space-lock.js +337 -0
  96. package/src/runtime/space-platform.js +105 -0
  97. package/src/runtime/space-source.js +281 -0
  98. package/src/runtime/space-test-server.js +327 -0
  99. package/src/runtime/space-verification.js +89 -0
  100. package/src/runtime/space-verify-browser-worker.js +51 -0
  101. package/src/runtime/space-view-manifest.js +254 -0
  102. package/src/runtime/sync-skills.js +1 -1
  103. package/src/runtime/transport.js +8 -1
  104. package/src/runtime/view-render.js +49 -0
  105. package/src/space-harness-host.ts +103 -0
  106. package/template/app/globals.css +1 -0
  107. package/src/command-specs/reports.js +0 -97
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { join } from 'node:path';
2
3
  import type {
3
4
  AgentTargets,
4
5
  LocalSkill,
@@ -6,8 +7,11 @@ import type {
6
7
  SkillSyncFailure,
7
8
  SyncPullResponse,
8
9
  SyncSettings,
10
+ SyncedSkill,
9
11
  } from "./types";
10
12
  import { normalizeAgentTargets } from "./types";
13
+ import { assertNativePull, checkedNativeReference, isNativeSkill, nativeState } from './native-identity';
14
+ import { bindNativeFolders, hasNativeFolderHistory, nativeLinkRows, nativeLocalReady, previousNativeState, retainMissingNativeState } from './native-plan';
11
15
  import {
12
16
  downloadSkillBundle,
13
17
  fetchSyncSettings,
@@ -59,6 +63,8 @@ export interface RunSkillSyncOptions {
59
63
  /** Electron's scheduled invocation honors the account preference. A manual
60
64
  * CLI sync always runs, even when automatic Desktop refresh is disabled. */
61
65
  honorSyncEnabled?: boolean;
66
+ /** All account paths, scanner roots and agent links use this explicit home. */
67
+ syncHome?: string;
62
68
  }
63
69
 
64
70
  const BASE_SKILL_NAMES = new Set(['notis-apps', 'notis-query', 'notis-cli']);
@@ -93,6 +99,8 @@ export interface MaterializeCloudSkillsResult {
93
99
  export interface MaterializeCloudSkillsOptions {
94
100
  /** Server-verified Notis identity, when Desktop's auth subject differs. */
95
101
  canonicalUserId?: string;
102
+ /** Previously authenticated settings must agree with the pull before I/O. */
103
+ syncSettings?: SyncSettings;
96
104
  /** Re-link only these cloud skills without removing or adopting other links. */
97
105
  relinkSkillNames?: readonly string[];
98
106
  }
@@ -134,6 +142,22 @@ const DEFAULT_RUN_SKILL_SYNC_DEPS: RunSkillSyncDependencies = {
134
142
  removeForeignAccountSymlinks,
135
143
  updateAgentTargets,
136
144
  };
145
+
146
+ export function isolatedLinkOptions(paths: ReturnType<typeof getSkillSyncPathsForUser>) {
147
+ if (!paths.isolatedNamespace && !paths.homeDir) return undefined;
148
+ const root = paths.isolatedNamespace
149
+ ? join(paths.syncRoot, 'environments', paths.isolatedNamespace, 'agents') : paths.homeDir!;
150
+ return { agentSkillDirs: { notis: paths.skillsDir, claude_code: join(root, '.claude', 'skills'),
151
+ cursor: join(root, '.cursor', 'skills'), codex: join(root, '.codex', 'skills') },
152
+ legacyGlobalSkillsDir: join(root, '.agents', 'skills') };
153
+ }
154
+
155
+ function assertSyncEnvironment(pull: SyncPullResponse, settings: SyncSettings): void {
156
+ if ((pull.sync_namespace || null) !== (settings.sync_namespace || null)
157
+ || (settings.sync_scope !== undefined && pull.sync_scope !== settings.sync_scope)) {
158
+ throw new Error('Skill sync environment changed. Existing mirrors were retained.');
159
+ }
160
+ }
137
161
  function toSkillMap(skills: LocalSkill[]): Map<string, LocalSkill> {
138
162
  return new Map(skills.map((skill) => [skill.name, skill]));
139
163
  }
@@ -175,6 +199,15 @@ export function shouldWriteCloudSkill(
175
199
  return true;
176
200
  }
177
201
 
202
+ if (isNativeSkill(cloudSkill)) {
203
+ if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
204
+ if (nativeLocalReady(cloudSkill, localSkill)) return false;
205
+ const nativePrevious = previousNativeState(previousState, cloudSkill);
206
+ return (localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash)
207
+ || (!localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id
208
+ && nativePrevious.folderHash === localSkill.folderHash);
209
+ }
210
+
178
211
  const previous = previousState.skills[skillName];
179
212
  if (previous?.folderHash === localSkill.folderHash
180
213
  && previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
@@ -182,10 +215,9 @@ export function shouldWriteCloudSkill(
182
215
  // Without a stored content baseline (skills served through a signed source URL keep
183
216
  // none, because that URL changes on every pull) the only other signal is
184
217
  // `skill_folder_hash`, which the server does NOT always compute the way the local
185
- // folder hash is computed. An app-published skill hashes its bundle entries, so that
186
- // value can never equal the hash of the extracted directory, and comparing the two
187
- // replaced the folder on every sync tick, deleting whatever a running skill had
188
- // written inside it. Compare cloud against cloud instead: rewrite when the cloud
218
+ // folder hash is computed. A bundle-entry hash can never equal the hash of the
219
+ // extracted directory, and comparing the two replaced the folder on every sync
220
+ // tick, deleting whatever a running skill had written inside it. Compare cloud against cloud instead: rewrite when the cloud
189
221
  // revision we last applied has actually moved.
190
222
  if (previous
191
223
  && previous.cloudContentHash === undefined
@@ -257,19 +289,29 @@ function buildSyncState(
257
289
  failedContentNames: ReadonlySet<string> = new Set(),
258
290
  appliedRevisions: AppliedCloudRevisions = {},
259
291
  appliedFiles: Record<string, string[] | undefined> = {},
292
+ previousState: NotisSyncState = { version: 1, lastSyncedAt: null, skills: {} },
260
293
  ): NotisSyncState {
261
294
  const localSkillMap = toSkillMap(localSkills);
262
295
  // The same row selection the writer used, so the state describes the revision that
263
296
  // is actually on disk rather than whichever duplicate row the server listed last.
264
- const skills = Object.fromEntries(
265
- selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
297
+ const skills = Object.fromEntries<SyncedSkill>(
298
+ selectCloudSkillsToApply(pullResponse.skills).map((skill): [string, SyncedSkill] => {
266
299
  const localSkill = localSkillMap.get(skill.name);
267
300
  const appliedCloudFolderHash = appliedRevisions[skill.name];
268
301
  const appliedFileList = appliedFiles[skill.name];
302
+ if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
303
+ const previous = previousNativeState(previousState, skill);
304
+ return [skill.name, { ...(previous || {
305
+ cloudId: skill.id, folderHash: localSkill?.nativeBaseFolderHash || '',
306
+ agentTargets: normalizeAgentTargets(skill.agent_targets), syncedAt: lastSyncedAt || new Date().toISOString(),
307
+ ...(localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}),
308
+ }), nativeUnavailable: true, verifiedAgentLinks: {} }];
309
+ }
269
310
  return [
270
311
  skill.name,
271
312
  {
272
313
  cloudId: skill.id,
314
+ ...nativeState(skill),
273
315
  folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
274
316
  agentTargets: normalizeAgentTargets(skill.agent_targets),
275
317
  verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
@@ -284,11 +326,18 @@ function buildSyncState(
284
326
  }),
285
327
  );
286
328
 
287
- return {
329
+ for (const local of localSkills) {
330
+ if (!local.nativeReference || Object.values(skills).some(row => row.cloudId === local.cloudId)) continue;
331
+ skills[local.name] = { cloudId: local.cloudId!, nativeReference: local.nativeReference,
332
+ nativeScope: local.nativeScope, nativeUnavailable: true, verifiedAgentLinks: {},
333
+ folderHash: local.nativeBaseFolderHash || '', agentTargets: normalizeAgentTargets(),
334
+ syncedAt: lastSyncedAt || new Date().toISOString() };
335
+ }
336
+ return retainMissingNativeState(previousState, {
288
337
  version: 1,
289
338
  lastSyncedAt,
290
339
  skills,
291
- };
340
+ });
292
341
  }
293
342
 
294
343
  function buildLocalSymlinkCandidates(
@@ -301,6 +350,10 @@ function buildLocalSymlinkCandidates(
301
350
  );
302
351
  const localOnlySkills = localSkills
303
352
  .filter((skill) => !cloudSkillNames.has(skill.name))
353
+ .filter(skill => !skill.nativeReference && !previousState.skills[skill.name]?.nativeReference
354
+ && !previousState.skills[skill.name]?.nativeUnavailable
355
+ && !hasNativeFolderHistory(previousState, skill.name)
356
+ && !/^native-[a-f0-9-]{36}$/.test(skill.name))
304
357
  .map((skill) => {
305
358
  const previous = previousState.skills[skill.name];
306
359
  return {
@@ -315,7 +368,7 @@ function buildLocalSymlinkCandidates(
315
368
  };
316
369
  });
317
370
 
318
- return [...pullResponse.skills, ...localOnlySkills];
371
+ return [...nativeLinkRows(pullResponse, localSkills), ...localOnlySkills];
319
372
  }
320
373
 
321
374
  function isEmptySyncState(state: NotisSyncState): boolean {
@@ -364,6 +417,7 @@ async function writePulledSkillsToScopedMirror(
364
417
  failures: SkillSyncFailure[] = [],
365
418
  writtenSkillNames: Set<string> = new Set(),
366
419
  appliedFilesByName: Record<string, string[]> = {},
420
+ acknowledgedNative: ReadonlyMap<string, string> = new Map(),
367
421
  ): Promise<number> {
368
422
  const localSkillMap = toSkillMap(localSkills);
369
423
  const warnSkillSync = (message: string, error: unknown): void => {
@@ -372,7 +426,12 @@ async function writePulledSkillsToScopedMirror(
372
426
 
373
427
  let downloaded = 0;
374
428
  for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
375
- if (!shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
429
+ const local = localSkillMap.get(cloudSkill.name);
430
+ const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
431
+ if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
432
+ if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
433
+ failures.push({ name: cloudSkill.name, error: 'Local native Skill changes were retained; resolve the version or identity conflict before using them.' });
434
+ }
376
435
  continue;
377
436
  }
378
437
 
@@ -382,7 +441,8 @@ async function writePulledSkillsToScopedMirror(
382
441
  deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
383
442
  // What the last write put there. Everything else in the folder was
384
443
  // produced locally and survives this one.
385
- previouslyApplied: previousState.skills[skill.name]?.appliedFiles,
444
+ previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
445
+ ...(isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}),
386
446
  }),
387
447
  onWarning: warnSkillSync,
388
448
  });
@@ -403,6 +463,8 @@ async function writePulledSkillsToScopedMirror(
403
463
  function assertSkillsPullAuthorized(
404
464
  pullResponse: SyncPullResponse,
405
465
  ): void {
466
+ if (!Array.isArray(pullResponse.skills)) throw new Error('Skill inventory is unavailable; existing files were retained.');
467
+ assertNativePull(pullResponse);
406
468
  if (
407
469
  pullResponse.entitlement_access?.code === "entitlement_upgrade_required"
408
470
  && pullResponse.entitlement_access.entitlement === "skills"
@@ -445,12 +507,14 @@ export async function materializeCloudSkillsForLocalShell(
445
507
  );
446
508
  }
447
509
 
448
- const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId);
449
- const pullResponse = await deps.pullSkills(serverUrl, jwt);
510
+ let pullResponse = await deps.pullSkills(serverUrl, jwt);
450
511
  assertSkillsPullAuthorized(pullResponse);
512
+ if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
513
+ const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, undefined, pullResponse.sync_namespace);
451
514
  const previousState = await deps.readSyncState(syncPaths);
452
515
 
453
516
  const localSkills = await deps.scanLocalSkills(syncPaths);
517
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
454
518
  const failedDownloads: SkillSyncFailure[] = [];
455
519
  const writtenSkillNames = new Set<string>();
456
520
  const appliedFilesByName: Record<string, string[]> = {};
@@ -478,9 +542,9 @@ export async function materializeCloudSkillsForLocalShell(
478
542
  }
479
543
  if (relinkSkillNames.size > 0) {
480
544
  const relinked = await deps.syncSymlinks(
481
- pullResponse.skills.filter((skill) => relinkSkillNames.has(skill.name)),
545
+ nativeLinkRows(pullResponse, finalLocalSkills).filter((skill) => relinkSkillNames.has(skill.name)),
482
546
  syncPaths.skillsDir,
483
- { removeUndesired: false },
547
+ { ...isolatedLinkOptions(syncPaths), removeUndesired: false },
484
548
  );
485
549
  failures.push(...(relinked.failures ?? []).filter(
486
550
  (failure) => !failedDownloads.some((download) => download.name === failure.name),
@@ -496,6 +560,7 @@ export async function materializeCloudSkillsForLocalShell(
496
560
  new Set(failedDownloads.map(item => item.name)),
497
561
  collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
498
562
  collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
563
+ previousState,
499
564
  );
500
565
  // Pull-only refresh is not an upload acknowledgement. Keep content baselines
501
566
  // unless we actually wrote cloud content, and retain cloud-missing entries so
@@ -511,17 +576,27 @@ export async function materializeCloudSkillsForLocalShell(
511
576
  }
512
577
  }
513
578
  materializedState.skills = { ...previousState.skills, ...materializedState.skills };
579
+ let removed = 0;
580
+ for (const [name, previous] of Object.entries(previousState.skills)) {
581
+ if (!previous.nativeReference && !previous.nativeUnavailable) continue;
582
+ const current = pullResponse.skills.find(skill => skill.id === previous.cloudId);
583
+ if (!current || !nativeLocalReady(current, finalLocalSkills.find(local => local.name === current.name))) {
584
+ removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
585
+ }
586
+ }
514
587
  await deps.writeSyncState(materializedState, syncPaths);
515
588
 
516
589
  return {
517
590
  materializedSkillNames: pullResponse.skills
518
591
  .filter((skill) => finalLocalSkills.some((local) => local.name === skill.name)
592
+ && (!isNativeSkill(skill) || (skill.status === 'active' && skill.agent_targets?.notis !== false
593
+ && nativeLocalReady(skill, finalLocalSkills.find(local => local.name === skill.name))))
519
594
  && !failedDownloads.some((failure) => failure.name === skill.name))
520
595
  .map((skill) => skill.name),
521
596
  pulled: pullResponse.skills.length,
522
597
  downloaded,
523
598
  deleted: 0,
524
- removed: 0,
599
+ removed,
525
600
  lastSyncedAt,
526
601
  ...(failures.length ? { failedLinks: failures } : {}),
527
602
  };
@@ -546,6 +621,7 @@ async function deactivateDeletedAgentSkills(
546
621
  "detectDeletedAgentSymlinks" | "updateAgentTargets" | "pullSkills"
547
622
  >,
548
623
  failures: SkillSyncFailure[],
624
+ linkOptions?: ReturnType<typeof isolatedLinkOptions>,
549
625
  ): Promise<number> {
550
626
  // First sync (incl. legacy migration) has no reliable "we created this link" signal, so we
551
627
  // cannot tell a user deletion apart from a never-created link — skip detection entirely.
@@ -554,9 +630,13 @@ async function deactivateDeletedAgentSkills(
554
630
  }
555
631
 
556
632
  const deletions = await deps.detectDeletedAgentSymlinks(
557
- pullResponse.skills,
633
+ pullResponse.skills.map(skill => {
634
+ const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : undefined;
635
+ return previous ? { ...skill, name: previous[0] } : skill;
636
+ }),
558
637
  previousState,
559
638
  skillsDir,
639
+ linkOptions,
560
640
  );
561
641
  if (deletions.length === 0) {
562
642
  return 0;
@@ -574,18 +654,24 @@ async function deactivateDeletedAgentSkills(
574
654
 
575
655
  const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
576
656
  assertSkillsPullAuthorized(fresh);
657
+ assertSyncEnvironment(fresh, pullResponse);
577
658
  Object.assign(pullResponse, fresh);
578
659
  let needsRefresh = false;
579
660
  let deactivated = 0;
580
661
  for (const [skillId, { skillName, agents }] of agentsBySkill) {
581
662
  const skill = pullResponse.skills.find((item) => item.id === skillId);
582
663
  const previous = previousState.skills[skillName];
583
- if (!skill?.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
664
+ if (!skill) continue;
665
+ const native = isNativeSkill(skill);
666
+ if (native) {
667
+ if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
668
+ } else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
584
669
  const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
585
670
  try {
586
- const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch, skill.updated_at);
671
+ const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch,
672
+ native ? undefined : skill.updated_at, native ? { reference: skill.native_reference!, settingsRevision: skill.native_settings!.revision } : undefined);
587
673
  if (saved.success !== true || !saved.updated_at?.trim()
588
- || saved.updated_at === skill.updated_at
674
+ || (!native && saved.updated_at === skill.updated_at)
589
675
  || !['notis', 'claude_code', 'cursor', 'codex'].every((agent) =>
590
676
  typeof saved.agent_targets?.[agent as keyof AgentTargets] === 'boolean')
591
677
  || ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
@@ -593,6 +679,7 @@ async function deactivateDeletedAgentSkills(
593
679
  }
594
680
  skill.agent_targets = saved.agent_targets;
595
681
  skill.updated_at = saved.updated_at;
682
+ if (native) needsRefresh = true;
596
683
  deactivated += agents.size;
597
684
  } catch (error) {
598
685
  needsRefresh = true;
@@ -603,6 +690,7 @@ async function deactivateDeletedAgentSkills(
603
690
  if (needsRefresh) {
604
691
  const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
605
692
  assertSkillsPullAuthorized(refreshed);
693
+ assertSyncEnvironment(refreshed, pullResponse);
606
694
  Object.assign(pullResponse, refreshed);
607
695
  }
608
696
  return deactivated;
@@ -633,11 +721,13 @@ export async function runSkillSync(
633
721
  "Cannot sync skills without a server-verified account identity.",
634
722
  );
635
723
  }
636
- const syncPaths = getSkillSyncPathsForUser(syncUserId);
637
- const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
638
- syncPaths.skillsDir,
639
- );
724
+ const syncPaths = getSkillSyncPathsForUser(syncUserId, undefined, syncSettings.sync_namespace, options.syncHome);
640
725
  if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
726
+ // Disabled sync intentionally performs no pull. Only the authenticated
727
+ // settings scope may remove foreign links; account mirrors stay untouched.
728
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
729
+ syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
730
+ );
641
731
  return {
642
732
  syncEnabled: false,
643
733
  pushed: 0,
@@ -655,6 +745,10 @@ export async function runSkillSync(
655
745
 
656
746
  let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
657
747
  assertSkillsPullAuthorized(pullResponse);
748
+ assertSyncEnvironment(pullResponse, syncSettings);
749
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
750
+ syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
751
+ );
658
752
 
659
753
  const cloudCuratedSkillNames = new Set(
660
754
  pullResponse.skills
@@ -664,50 +758,63 @@ export async function runSkillSync(
664
758
  const protectedSkillNames = new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
665
759
  const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
666
760
  const assignmentFailures: SkillSyncFailure[] = [];
667
- const deactivated = syncSettings.agent_targets_conditional_updates === true
761
+ const deactivated = !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true
668
762
  ? await deactivateDeletedAgentSkills(
669
- serverUrl, jwt, pullResponse, scopedState, scopedState, syncPaths.skillsDir, deps, assignmentFailures,
763
+ serverUrl, jwt, pullResponse, scopedState, scopedState, syncPaths.skillsDir, deps, assignmentFailures, isolatedLinkOptions(syncPaths),
670
764
  )
671
765
  : 0;
766
+ assertSyncEnvironment(pullResponse, syncSettings);
672
767
  const authUserId = decodeJwtSubject(jwt);
673
768
  let previousAuthState: NotisSyncState | null = null;
674
- if (authUserId && authUserId !== syncUserId) {
675
- const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
769
+ if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
770
+ const previousAuthPaths = getSkillSyncPathsForUser(authUserId, undefined, undefined, options.syncHome);
676
771
  previousAuthState = await deps.readSyncState(previousAuthPaths);
677
772
  await deps.gatherTopLevelLocalSkills(syncPaths, {
678
773
  sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
679
774
  protectedSkillNames,
680
775
  });
681
776
  }
682
- await deps.gatherTopLevelLocalSkills(syncPaths, {
683
- protectedSkillNames,
684
- });
777
+ if (!syncPaths.isolatedNamespace) {
778
+ await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
779
+ }
685
780
  const localSkills = (await deps.scanLocalSkills(syncPaths))
686
781
  .filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
687
782
  const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
688
783
  localSkills,
689
784
  scopedState,
690
- isEmptySyncState(scopedState)
785
+ !syncPaths.isolatedNamespace && isEmptySyncState(scopedState)
691
786
  ? (!previousAuthState || isEmptySyncState(previousAuthState)
692
787
  ? await deps.readLegacySyncState(syncPaths)
693
788
  : previousAuthState)
694
789
  : null,
695
790
  ));
791
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
696
792
  const gatheredSymlinkResult = await deps.syncSymlinks(
697
793
  buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
698
794
  syncPaths.skillsDir,
795
+ isolatedLinkOptions(syncPaths),
699
796
  );
700
797
  const pushCandidates = getPushCandidates(
701
798
  localSkills,
702
799
  previousState,
703
800
  cloudCuratedSkillNames,
704
801
  new Set(pullResponse.skills.map((skill) => skill.name)),
705
- new Set(pullResponse.skills.filter((skill) => skill.app_owned || skill.owner_app_id).map((skill) => skill.name)),
802
+ pullResponse.skills,
706
803
  );
707
804
 
708
805
  const failedPushes: SkillSyncFailure[] = [];
806
+ const acknowledgedNative = new Map<string, string>();
807
+ let writeLocalSkills = localSkills;
709
808
  if (pushCandidates.length > 0) {
710
809
  const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
810
+ for (const result of pushResult.skills || []) {
811
+ const saved = result as { id?: string; native_reference?: unknown };
812
+ const candidate = pushCandidates.find(local => local.nativeReference && local.cloudId === saved.id);
813
+ if (candidate && saved.native_reference) {
814
+ checkedNativeReference(saved.native_reference, candidate.cloudId);
815
+ acknowledgedNative.set(candidate.cloudId!, candidate.folderHash);
816
+ }
817
+ }
711
818
  if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
712
819
  failedPushes.push(...pushResult.failed);
713
820
  console.warn(
@@ -717,6 +824,9 @@ export async function runSkillSync(
717
824
  }
718
825
  pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
719
826
  assertSkillsPullAuthorized(pullResponse);
827
+ assertSyncEnvironment(pullResponse, syncSettings);
828
+ writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter(skill => !BASE_SKILL_NAMES.has(skill.name));
829
+ pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
720
830
  }
721
831
 
722
832
  // Delete phase: remove skills that were previously synced but are no longer in the cloud
@@ -724,8 +834,14 @@ export async function runSkillSync(
724
834
  let deleted = 0;
725
835
  for (const skillName of Object.keys(previousState.skills)) {
726
836
  if (!cloudSkillNames.has(skillName)) {
837
+ const previous = previousState.skills[skillName];
838
+ if (previous.nativeReference || previous.nativeUnavailable
839
+ || pullResponse.skills.some(skill => isNativeSkill(skill) && skill.id === previous.cloudId)) {
840
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
841
+ continue;
842
+ }
727
843
  await deps.deleteLocalSkill(skillName, syncPaths);
728
- await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
844
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
729
845
  deleted += 1;
730
846
  }
731
847
  }
@@ -735,13 +851,14 @@ export async function runSkillSync(
735
851
  const appliedFilesByName: Record<string, string[]> = {};
736
852
  const downloaded = await writePulledSkillsToScopedMirror(
737
853
  pullResponse,
738
- localSkills,
854
+ writeLocalSkills,
739
855
  previousState,
740
856
  syncPaths,
741
857
  deps,
742
858
  failedDownloads,
743
859
  writtenSkillNames,
744
860
  appliedFilesByName,
861
+ acknowledgedNative,
745
862
  );
746
863
 
747
864
  const finalLocalSkills = (await deps.scanLocalSkills(syncPaths))
@@ -749,6 +866,7 @@ export async function runSkillSync(
749
866
  const symlinkResult = await deps.syncSymlinks(
750
867
  buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
751
868
  syncPaths.skillsDir,
869
+ isolatedLinkOptions(syncPaths),
752
870
  );
753
871
  const verifiedLinks = { ...(symlinkResult.verifiedAgentLinks ?? {}) };
754
872
  for (const failure of failedDownloads) delete verifiedLinks[failure.name];
@@ -763,6 +881,7 @@ export async function runSkillSync(
763
881
  new Set(failedDownloads.map(item => item.name)),
764
882
  collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
765
883
  collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
884
+ previousState,
766
885
  ),
767
886
  syncPaths,
768
887
  );