@yugoo-design/companion 0.1.0-beta.3 → 0.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Yugoo Design Companion — beta
2
2
 
3
- Package: `@yugoo-design/companion@0.1.0-beta.3`.
3
+ Package: `@yugoo-design/companion@0.1.0-beta.5`.
4
4
 
5
5
  Yugoo Design (语构设计系统) is an interactive design plugin for native Agent Hosts.
6
6
  It combines domain methods, Design Projects, versioned Design Files and shared
@@ -11,19 +11,86 @@ This beta is experimental. Native installation, tools, Panel saving, KAG and GRA
11
11
  must be verified for the actual host version and platform. A successful package
12
12
  installation or runtime-load check does not establish all of those capabilities.
13
13
 
14
- Use Node.js `>=22.19.0 <23` and a durable npm installation directory. This source
15
- candidate uses the packaged native Git library for runtime versioning; system Git
16
- is needed only for the explicit CLI diagnostic backend and development fixtures.
17
- The installed native binding must support the actual operating system and CPU;
18
- missing support is reported rather than silently spawning system Git. Resolve the
14
+ Use Node.js `>=22.19.0 <23` and a durable npm or pnpm installation directory. The current
15
+ release uses the packaged native Git library for normal Design versioning without
16
+ system Git. It bundles Windows x64 MSVC and Linux x64 GNU runtimes; other platforms
17
+ are unsupported. System Git is separate and needed only for the explicit CLI
18
+ diagnostic backend and development fixtures. Missing native support is reported
19
+ rather than silently spawning system Git. Resolve the
19
20
  selected beta to the exact version and integrity before acquisition; keep the
20
21
  previous installed version available during updates. Do not substitute `latest`.
21
22
 
22
23
  ```sh
23
- npm install --ignore-scripts --save-exact @yugoo-design/companion@0.1.0-beta.3 --registry=https://registry.npmjs.org
24
+ ONNXRUNTIME_NODE_INSTALL_CUDA=skip npm install --ignore-scripts=false --save-exact @yugoo-design/companion@0.1.0-beta.5 --registry=https://registry.npmjs.org
24
25
  npx --no-install design-system --help
25
26
  ```
26
27
 
28
+ Alternatively, use an existing pnpm 11 installation (npm remains supported).
29
+ In the dedicated, empty version directory, create `pnpm-workspace.yaml` with
30
+ these local settings before acquisition:
31
+
32
+ ```yaml
33
+ nodeLinker: hoisted
34
+ allowBuilds:
35
+ onnxruntime-node: true
36
+ sharp: true
37
+ ```
38
+
39
+ ```sh
40
+ ONNXRUNTIME_NODE_INSTALL_CUDA=skip pnpm add --save-exact @yugoo-design/companion@0.1.0-beta.5 --registry=https://registry.npmjs.org
41
+ pnpm exec design-system --help
42
+ ```
43
+
44
+ Run either acquisition in its own retained version directory, not the Design
45
+ Project or host profile. Do not mix npm and pnpm lockfiles in that directory.
46
+ The hoisted layout is the compatibility route for Companion's current native
47
+ dependency inspection; other pnpm layouts are not certified for every host.
48
+ pnpm's explicit build permissions above allow the current native dependencies'
49
+ installation scripts; do not disable all scripts or enable all future dependency
50
+ builds. Review any newly reported build requirement before allowing it. Retain
51
+ the complete pnpm installation layout; do not copy just a linked
52
+ package directory or invoke Node with `--preserve-symlinks` flags.
53
+
54
+ An explicitly selected HTTPS npm mirror is supported, for example
55
+ `--registry=https://registry.npmmirror.com`. Use it both when acquiring this
56
+ package and when invoking `design-system install --host dsh --registry=<url>`
57
+ with the required host inputs. The latter controls the official DSH CLI's
58
+ package download registry; it does not replace that CLI with npm or pnpm.
59
+ The exact aggregate version still comes from `release-hosts.json`, whose registry
60
+ remains the canonical release origin/default. The plan and receipt separately
61
+ record the effective mirror and bind it through apply and owned updates. There
62
+ is no automatic registry fallback or global registry edit. Verify the exact
63
+ mirror version and integrity against the selected release before installation;
64
+ missing or stale mirror packages are not permission to change version or source.
65
+ Registry selection does not redirect separate embedding-model or upstream
66
+ installation-script downloads to that mirror.
67
+
68
+ First-release local embedding runs on CPU. Set the supported
69
+ `ONNXRUNTIME_NODE_INSTALL_CUDA=skip` environment before acquisition to skip ONNX
70
+ Runtime's extra CUDA download while retaining normal dependency lifecycle
71
+ scripts. Preserve the upstream packages and other environment values. The
72
+ installed package cannot configure its acquiring parent npm/npx process; a bare
73
+ npm/npx acquisition does not apply this policy automatically. Keep the same
74
+ policy on later host-installation children that acquire dependencies. The
75
+ maintained DSH adapter passes it to the official CLI in Web and Desktop; older
76
+ installed adapters may need the caller to supply it. This does not establish
77
+ installation success or remove the separate embedding-model download/cache step.
78
+ The default product does not provision GPU runtimes; optional GPU support and
79
+ configurable or user-local model selection are future work.
80
+
81
+ In Windows PowerShell, scope the setting to acquisition and restore its previous
82
+ process value:
83
+
84
+ ```powershell
85
+ $previousOrtCuda = [Environment]::GetEnvironmentVariable('ONNXRUNTIME_NODE_INSTALL_CUDA', 'Process')
86
+ try {
87
+ $env:ONNXRUNTIME_NODE_INSTALL_CUDA = 'skip'
88
+ npm install --ignore-scripts=false --save-exact @yugoo-design/companion@0.1.0-beta.5 --registry=https://registry.npmjs.org
89
+ } finally {
90
+ [Environment]::SetEnvironmentVariable('ONNXRUNTIME_NODE_INSTALL_CUDA', $previousOrtCuda, 'Process')
91
+ }
92
+ ```
93
+
27
94
  Ask the Agent in your current host to install or update Yugoo Design as one
28
95
  product. It should identify the host from its actual execution context or your
29
96
  explicit choice, not from which CLIs happen to be installed. If identity is
package/bin/installer.mjs CHANGED
@@ -15,7 +15,7 @@ import { checkRuntime, inspectRuntime, assertGitRuntimeReceipt } from './npm-run
15
15
  import { runDesktopDshInstaller } from './npm-installer-dsh-desktop.mjs'
16
16
  import { desktopPreparationMain, desktopPreparationReuseMain } from './npm-desktop-preparation.mjs'
17
17
  import { agentInstallation, AGENT_INSTALL_HOSTS, DESKTOP_INSTALL_OPTIONS } from './npm-agent-installation.mjs'
18
- import { createDshInstallerAdapter, planDshInstallation, applyDshInstallation } from './npm-installer-dsh.mjs'
18
+ import { createDshInstallerAdapter, planDshInstallation, applyDshInstallation, validateReleaseHosts } from './npm-installer-dsh.mjs'
19
19
 
20
20
  export const INSTALL_HOSTS = ['dsh','codex','claude-code','qoder','codex-desktop','qwenwork']
21
21
  const PLUGIN = 'yugoo-design'
@@ -623,32 +623,15 @@ async function recoverOwnedInstallation(receipt,{adapter,ownRecovery,uninstall=f
623
623
  }
624
624
 
625
625
  const DSH_FLAGS = ['dsh-config','profile','aggregate-selector','registry']
626
- // Only DSH consumes this local product mapping. Older candidates retain their
627
- // explicit selector/registry route; preparation values never imply a release.
628
- export function validateReleaseHosts(config) {
629
- assert.ok(config && typeof config==='object' && !Array.isArray(config),'Release hosts object required')
630
- assert.deepEqual(Object.keys(config).sort(),['hosts','schemaVersion'],'Unknown release hosts fields')
631
- assert.equal(config.schemaVersion,1,'Unsupported release hosts schema')
632
- assert.ok(config.hosts && typeof config.hosts==='object' && !Array.isArray(config.hosts),'Release hosts map required')
633
- assert.deepEqual(Object.keys(config.hosts),['dsh'],'Release mapping currently supports DSH only')
634
- const dsh=config.hosts.dsh
635
- assert.ok(dsh && typeof dsh==='object' && !Array.isArray(dsh),'DSH release mapping required')
636
- assert.deepEqual(Object.keys(dsh).sort(),['aggregatePackage','aggregateVersion','registry'],'Unknown DSH release fields')
637
- assert.match(dsh.aggregatePackage ?? '',/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u,'Exact aggregate package name required')
638
- if(dsh.aggregateVersion===null && dsh.registry===null)return null
639
- assert.match(dsh.aggregateVersion ?? '',/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,'Exact aggregate version required')
640
- assert.equal(typeof dsh.registry,'string','Bound HTTPS registry required')
641
- const url=new URL(dsh.registry)
642
- assert.ok(url.protocol==='https:' && !url.username && !url.password && !url.search && !url.hash,'Credential-free HTTPS registry required')
643
- return {aggregateSelector:`${dsh.aggregatePackage}@${dsh.aggregateVersion}`,registry:url.href}
644
- }
626
+ export { validateReleaseHosts }
645
627
  async function dshReleaseSelection(values,npmRoot) {
646
628
  const bytes=await optional(join(npmRoot,'release-hosts.json'))
647
629
  const mapped=bytes===null?null:validateReleaseHosts(JSON.parse(bytes))
648
630
  if(mapped) {
649
631
  if(Object.hasOwn(values,'aggregate-selector'))assert.equal(values['aggregate-selector'],mapped.aggregateSelector,'Aggregate override differs from product release')
650
- if(Object.hasOwn(values,'registry'))assert.equal(new URL(values.registry).href,mapped.registry,'Registry override differs from product release')
651
- return mapped
632
+ // The release binds the package/version and default origin, not the user's
633
+ // explicit download mirror. The owning planner validates and pins transport.
634
+ return {...mapped,registry:values.registry ?? mapped.registry}
652
635
  }
653
636
  for(const key of ['aggregate-selector','registry'])assert.ok(typeof values[key]==='string' && values[key],`DSH release mapping is unbound or missing; explicit --${key} required for compatibility installation`)
654
637
  return {aggregateSelector:values['aggregate-selector'],registry:values.registry}
@@ -667,7 +650,7 @@ async function dshConfiguration(path) {
667
650
  }
668
651
  return config
669
652
  }
670
- async function dshOutputDirectory(path,{npmRoot,config,configPath,planPath}) {
653
+ async function dshOutputDirectory(path,{npmRoot,config,configPath,planPath,previousProductRoot}) {
671
654
  assert.ok(typeof path==='string'&&isAbsolute(path),'Explicit absolute external receipt directory required')
672
655
  const output=resolve(path)
673
656
  await safePath(output)
@@ -678,7 +661,7 @@ async function dshOutputDirectory(path,{npmRoot,config,configPath,planPath}) {
678
661
  try { assert.equal(await realpath(ancestor),ancestor,'Output ancestor realpath mismatch'); break }
679
662
  catch(error){if(error.code!=='ENOENT')throw error;ancestor=dirname(ancestor)}
680
663
  }
681
- const roots=[resolve(npmRoot),resolve(config.dshHome)]
664
+ const roots=[resolve(npmRoot),resolve(config.dshHome),...previousProductRoot ? [resolve(previousProductRoot)] : []]
682
665
  for(const key of ['node','cli','hostManifest','loader','loaderManifest', ...config.desktopHostManifest ? ['desktopHostManifest'] : []]) {
683
666
  roots.push(dirname(resolve(config[key].path)),dirname(await realpath(config[key].path)))
684
667
  }
@@ -718,9 +701,12 @@ async function dshInstallerEntry(values,{npmRoot,dshAdapterFactory,args,signal,d
718
701
  const name=selection.aggregateSelector.slice(0,selection.aggregateSelector.lastIndexOf('@'))
719
702
  aggregateDirectory=dirname(await realpath(createRequire(join(npmRoot,'package.json')).resolve(name+'/package.json')))
720
703
  } catch(error) { if(error.code!=='MODULE_NOT_FOUND')throw error }
721
- const options={adapter,profile:values.profile,...selection,...aggregateDirectory ? {aggregateDirectory} : {}}
704
+ const options={adapter,profile:values.profile,...selection,productRoot:await realpath(resolve(npmRoot)),
705
+ ...values['previous-receipt'] ? {previousReceipt:values['previous-receipt']} : {},
706
+ ...aggregateDirectory ? {aggregateDirectory} : {}}
722
707
  if(values['dry-run']) {
723
708
  const plan=await planDshInstallation(options)
709
+ if(plan.previous)await dshOutputDirectory(output,{npmRoot,config,configPath,previousProductRoot:plan.previous.product.npmRoot})
724
710
  const planFile=await writeDshOutput(output,plan.planSha256+'.dsh-plan.json',plan)
725
711
  return {...plan,planFile}
726
712
  }
@@ -731,14 +717,27 @@ async function dshInstallerEntry(values,{npmRoot,dshAdapterFactory,args,signal,d
731
717
  assert.equal(plan.profile,options.profile,'Saved DSH profile mismatch')
732
718
  assert.equal(plan.aggregateSelector,options.aggregateSelector,'Saved aggregate selector mismatch')
733
719
  assert.equal(plan.registry,new URL(options.registry).href,'Saved registry mismatch')
720
+ assert.equal(plan.previousReceipt,options.previousReceipt,'Saved previous ownership reference mismatch')
734
721
  assert.equal(plan.protocol.scope,adapter.scope,'Saved adapter scope mismatch')
735
722
  const fresh=await planDshInstallation(options)
723
+ if(fresh.previous)await dshOutputDirectory(output,{npmRoot,config,configPath,previousProductRoot:fresh.previous.product.npmRoot})
736
724
  assert.deepEqual(plan.protocol,fresh.protocol,'Saved official pins/home/version mismatch')
737
725
  assert.equal(plan.planSha256,fresh.planSha256,'Saved DSH plan or profile drifted')
738
- const result=await applyDshInstallation(plan,{adapter,signal,timeoutMs:dshInstallTimeoutMs,logDirectory:output})
739
- const receipt={...result,host:'dsh',route:'official-dsh-bundle',planSha256:plan.planSha256}
740
- const receiptFile=await writeDshOutput(output,randomUUID()+'.dsh-result.json',receipt)
741
- return {...receipt,receiptFile}
726
+ const id=randomUUID(),receiptPath=join(output,id+'.dsh-result.json')
727
+ const preparedRecordFile=await writeDshOutput(output,id+'.dsh-prepared.json',{
728
+ schemaVersion:2,kind:'dsh-installation-prepared',id,planSha256:plan.planSha256,planSnapshot:plan,
729
+ receiptPath,ownershipEstablished:false,phase:'prepared',retrySafe:false})
730
+ try {
731
+ const result=await applyDshInstallation(plan,{adapter,signal,timeoutMs:dshInstallTimeoutMs,logDirectory:output})
732
+ const body={...result,id,receiptPath,preparedRecordFile}
733
+ const receipt={...body,receiptSha256:digest(body)}
734
+ const receiptFile=await writeDshOutput(output,id+'.dsh-result.json',receipt)
735
+ return {...receipt,receiptFile}
736
+ } catch(error) {
737
+ // The native command may already have changed the profile. Keep the exact
738
+ // prepared record discoverable without fabricating a successful terminal receipt.
739
+ throw Object.assign(error,{receiptPath:preparedRecordFile,preparedRecordFile,planFile:values.plan})
740
+ }
742
741
  }
743
742
 
744
743
  /** Reuse native session plugin loading. No registration, copying, or host launch. */
@@ -832,13 +831,13 @@ export async function installerMain(args,{npmRoot,userRoot=homedir(),adapter=cod
832
831
  '--dsh-config <file>':'DSH install only: absolute JSON with dshHome, official node/CLI/Loader file pins and host/Loader versions; desktopHostManifest selects the official Electron Desktop CLI. No scope or runner override.',
833
832
  '--profile <name>':'DSH install only: explicitly selected existing profile; never initialized implicitly.',
834
833
  '--aggregate-selector <name@version>':'DSH install only: optional exact aggregate override matching the product release mapping; required for old or unbound candidates.',
835
- '--registry <https-url>':'DSH install only: optional credential-free HTTPS registry override matching the product mapping; required for old or unbound candidates.',
834
+ '--registry <https-url>':'DSH install only: explicit credential-free HTTPS download registry or mirror; defaults to the product mapping. Preserves the exact mapped aggregate and binds the saved plan/receipt; no automatic fallback. Required for old or unbound candidates.',
836
835
  '--destination <directory>':'QwenWork/WorkBuddy direct install: exclusive external preparation directory, or the same owned directory with --previous-receipt.',
837
836
  '--suite-version <version>':'QwenWork/WorkBuddy: exact native suite version, separate from the acquired npm version.',
838
837
  '--node-executable <path>':'QwenWork/WorkBuddy: absolute Windows Node path used by the imported suite.',
839
838
  '--qwenwork-runtime <file>':'QwenWork: verified native executable/worker/Node paths and byte pins. Agent obtains these through supported native inspection; no credentials.',
840
839
  '--separate-mcp':'QwenWork: emit one external connector instead of bundling it when organization policy requires this.',
841
- '--previous-receipt <file>':'Desktop: verify and reuse this owned preparation for a same-version repeat, or prepare a strictly newer native suite/npm version using its update boundary.',
840
+ '--previous-receipt <file>':'DSH: successful source-bound installation receipt for an owned update or repeat; sparse/manual receipts cannot be adopted. Desktop: reuse an owned preparation or prepare a newer native suite/npm version.',
842
841
  '--npm-root-windows, --destination-windows':'Desktop: explicit Windows path mappings required when preparing from another platform; native Windows paths must match actual roots.',
843
842
  },
844
843
  limitations:['plugin-directory is an optional session-only diagnostic, not installation. Codex requires an admitted adapter; DSH requires pinned public CLI inputs. Desktop preparation needs native import and activation. Exit 2 means host selection or inputs are required; exit 0 does not establish a usable native session.',
@@ -853,7 +852,8 @@ export async function installerMain(args,{npmRoot,userRoot=homedir(),adapter=cod
853
852
  desktopPrepare:nextArgs=>desktopPreparationMain(nextArgs,{npmRoot}),
854
853
  desktopReuse:nextArgs=>desktopPreparationReuseMain(nextArgs,{npmRoot})})
855
854
  }
856
- for(const key of DESKTOP_INSTALL_OPTIONS)assert.ok(!Object.hasOwn(values,key),`--${key} is only valid for direct desktop install`)
855
+ for(const key of DESKTOP_INSTALL_OPTIONS)if(!(key==='previous-receipt'&&values.host==='dsh'&&positionals[0]==='install'))
856
+ assert.ok(!Object.hasOwn(values,key),`--${key} is only valid for direct desktop install`)
857
857
  if(positionals[0]==='plugin-directory'&&['claude-code','qoder'].includes(values.host)) {
858
858
  assert.deepEqual(Object.keys(values),['host'],'Native directory entry accepts only --host; transaction/runtime options must not be silently ignored')
859
859
  return nativePluginDirectory({npmRoot,host:values.host})
@@ -884,7 +884,7 @@ export async function installerMain(args,{npmRoot,userRoot=homedir(),adapter=cod
884
884
  if(values.apply)assert.ok(values.plan&&values['runtime-receipt']&&!values['receipt-dir']&&!values['disable-embedding'],'Claude apply requires saved plan and runtime receipt only')
885
885
  else assert.ok(values['receipt-dir']&&!values.plan&&!values['runtime-receipt'],'Claude dry-run requires external receipt directory only')
886
886
  } else assert.ok(values.receipt&&!values.apply&&!values['dry-run']&&!values.plan&&!values['receipt-dir']&&!values['runtime-receipt']&&!values['disable-embedding'],'Claude recovery/uninstall requires only persisted receipt')
887
- const native=await claudeAdapterFactory({executable:values['claude-executable'],userRoot:profile,scope:values['dry-run']?'plan-only':'production'})
887
+ const native=await claudeAdapterFactory({executable:values['claude-executable'],userRoot:profile,nativeCwd:resolve(npmRoot),scope:values['dry-run']?'plan-only':'production'})
888
888
  if(command==='recover'||command==='uninstall') {
889
889
  const receipt=JSON.parse(await optional(resolve(values.receipt)))
890
890
  assert.equal(receipt.npmRoot,resolve(npmRoot),'Claude receipt npm root mismatch');assert.equal(receipt.userRoot,profile,'Claude receipt profile mismatch')
@@ -8,7 +8,7 @@ export const AGENT_INSTALL_HOSTS = ['dsh', 'codex', 'claude-code', 'qoder', 'qwe
8
8
  export const DESKTOP_INSTALL_OPTIONS = ['destination', 'node-executable', 'suite-version', 'npm-root-windows',
9
9
  'destination-windows', 'qwenwork-runtime', 'separate-mcp', 'previous-receipt']
10
10
  const nativeOptions = {
11
- dsh: ['host', 'receipt-dir', 'dsh-config', 'profile', 'aggregate-selector', 'registry'],
11
+ dsh: ['host', 'receipt-dir', 'dsh-config', 'profile', 'aggregate-selector', 'registry', 'previous-receipt'],
12
12
  codex: ['host', 'receipt-dir', 'codex-executable', 'user-root', 'disable-embedding'],
13
13
  'claude-code': ['host', 'receipt-dir', 'claude-executable', 'user-root', 'disable-embedding'],
14
14
  qoder: ['host', 'receipt-dir', 'qoder-executable', 'qoder-config-dir'],
@@ -57,34 +57,54 @@ async function executableFence(s) {
57
57
  assert.ok((await lstat(exe)).isFile(),'Claude executable must remain regular')
58
58
  assert.equal(sha(await readFile(exe)),s.identity.sha256,'Claude executable changed')
59
59
  }
60
+ async function nativeCwdFence(s) {
61
+ const cwd=s.identity.nativeCwd
62
+ await assertRuntimePath(cwd)
63
+ assert.equal(await realpath(cwd),cwd,'Claude canonical native working directory changed')
64
+ const info=await lstat(cwd)
65
+ assert.ok(info.isDirectory(),'Claude native working directory must be a directory')
66
+ assert.deepEqual({dev:info.dev,ino:info.ino},s.nativeCwdIdentity,'Claude native working directory replaced')
67
+ assert.notEqual(cwd,s.userRoot,'Claude native working directory must differ from HOME')
68
+ for(const name of ['settings.json','settings.local.json']) {
69
+ const file=join(cwd,'.claude',name)
70
+ await assertRuntimePath(file)
71
+ try { await lstat(file);assert.fail('Claude native working directory contains direct .claude configuration') }
72
+ catch(error) { if(error.code!=='ENOENT')throw error }
73
+ }
74
+ }
60
75
  function protocolIdentity(s) { const {scope,...identity}=s.identity;return identity }
61
76
  async function state(adapter) {
62
77
  const s=adapters.get(adapter);assert.ok(s,'Unrecognized Claude adapter')
63
- await executableFence(s);return s
78
+ await executableFence(s);await nativeCwdFence(s);return s
64
79
  }
65
80
  async function call(s,args) {
66
- await executableFence(s)
81
+ await executableFence(s);await nativeCwdFence(s)
67
82
  let result
68
- try { result=await s.run(s.identity.executable,args,{cwd:s.userRoot,env:s.env,timeout:30000,maxBuffer:1048576}) }
69
- finally { await executableFence(s) }
83
+ try { result=await s.run(s.identity.executable,args,{cwd:s.identity.nativeCwd,env:s.env,timeout:30000,maxBuffer:1048576}) }
84
+ finally { await executableFence(s);await nativeCwdFence(s) }
70
85
  assert.ok(result.code===undefined||result.code===0,'Claude command failed')
71
86
  assert.ok(!result.signal,'Claude command signalled');return result.stdout??''
72
87
  }
73
88
  /** Read-only factory. Injected runners are confined to explicitly isolated profiles. */
74
- export async function createClaudeNativeAdapter({executable,userRoot,scope='plan-only',run=execute}) {
89
+ export async function createClaudeNativeAdapter({executable,userRoot,nativeCwd,scope='plan-only',run=execute}) {
75
90
  assert.ok(['plan-only','isolated-acceptance','isolated-test-contract','production'].includes(scope),'Unknown Claude scope')
76
91
  assert.ok(isAbsolute(executable??'')&&isAbsolute(userRoot??''),'Explicit Claude executable/profile required')
92
+ assert.ok(isAbsolute(nativeCwd??''),'Explicit Claude native working directory required')
93
+ assert.equal(resolve(nativeCwd),nativeCwd,'Explicit canonical Claude native working directory required')
77
94
  const exe=await realpath(executable),home=resolve(userRoot)
78
95
  await assertRuntimePath(exe);await assertRuntimePath(home)
79
96
  assert.equal(await realpath(home),home);assert.ok((await lstat(home)).isDirectory())
97
+ await assertRuntimePath(nativeCwd);assert.equal(await realpath(nativeCwd),nativeCwd,'Claude canonical native working directory changed')
98
+ const cwdInfo=await lstat(nativeCwd)
99
+ assert.ok(cwdInfo.isDirectory(),'Claude native working directory must be a directory')
80
100
  if(scope.startsWith('isolated-'))assert.notEqual(home,resolve(homedir()),'Isolated Claude cannot use actual profile')
81
101
  else { assert.equal(run,execute,'Real profile cannot use injected transport');assert.equal(home,resolve(homedir()),'Real profile identity required') }
82
- const identity={executable:exe,sha256:sha(await readFile(exe)),userRoot:home,configDir:locations(home).config,platform:process.platform,arch:process.arch,scope}
102
+ const identity={executable:exe,sha256:sha(await readFile(exe)),userRoot:home,configDir:locations(home).config,nativeCwd,platform:process.platform,arch:process.arch,scope}
83
103
  const config=identity.configDir
84
104
  const env={HOME:home,CLAUDE_CONFIG_DIR:config,PATH:dirname(process.execPath)+':/usr/bin:/bin',
85
105
  XDG_CONFIG_HOME:join(home,'.config'),XDG_CACHE_HOME:join(home,'.cache'),XDG_DATA_HOME:join(home,'.local/share'),
86
106
  TMPDIR:join(config,'tmp'),LANG:'C.UTF-8',LC_ALL:'C.UTF-8',DISABLE_AUTOUPDATER:'1',NODE_OPTIONS:'',NODE_PATH:''}
87
- const s={identity,userRoot:home,run,env}
107
+ const s={identity,userRoot:home,run,env,nativeCwdIdentity:{dev:cwdInfo.dev,ino:cwdInfo.ino}}
88
108
  const version=(await call(s,['--version'])).trim().match(/^(\d+\.\d+\.\d+(?:[-+][\w.-]+)?) \(Claude Code\)$/)
89
109
  assert.ok(version,'Expected Claude Code native executable');identity.version=version[1]
90
110
  // Check the native interface we actually use, not one developer's binary hash.
@@ -169,6 +189,7 @@ async function lock(directory,id,fn,recovery=false) {
169
189
  }
170
190
  function validatePlan(plan) {
171
191
  const {planSha256,...body}=plan;assert.equal(digest(body),planSha256,'Claude plan body digest mismatch')
192
+ assert.ok(typeof plan.protocol?.nativeCwd==='string','Claude protocol missing nativeCwd; use the retained installed implementation for its existing receipts')
172
193
  assert.equal(plan.host,'claude-code');assert.equal(plan.scope,'user');pluginName(plan.selector)
173
194
  for(const key of ['npmRoot','userRoot','receiptDirectory'])assert.ok(isAbsolute(plan[key])&&resolve(plan[key])===plan[key],'Noncanonical Claude plan path')
174
195
  assert.ok(!beneath(plan.npmRoot,plan.receiptDirectory)&&!beneath(plan.paths.target,plan.receiptDirectory),'Receipt overlaps owned inputs')
@@ -98,6 +98,170 @@ async function checkPin(pin) {
98
98
  assert.equal(sha(await regular(await realpath(pin.path))), pin.sha256, 'Official file pin changed')
99
99
  }
100
100
 
101
+ // Only DSH consumes this local product mapping. Older candidates retain their
102
+ // explicit selector/registry route; preparation values never imply a release.
103
+ export function validateReleaseHosts(config) {
104
+ assert.ok(config && typeof config==='object' && !Array.isArray(config),'Release hosts object required')
105
+ assert.deepEqual(Object.keys(config).sort(),['hosts','schemaVersion'],'Unknown release hosts fields')
106
+ assert.equal(config.schemaVersion,1,'Unsupported release hosts schema')
107
+ assert.ok(config.hosts && typeof config.hosts==='object' && !Array.isArray(config.hosts),'Release hosts map required')
108
+ assert.deepEqual(Object.keys(config.hosts),['dsh'],'Release mapping currently supports DSH only')
109
+ const dsh=config.hosts.dsh
110
+ assert.ok(dsh && typeof dsh==='object' && !Array.isArray(dsh),'DSH release mapping required')
111
+ assert.deepEqual(Object.keys(dsh).sort(),['aggregatePackage','aggregateVersion','registry'],'Unknown DSH release fields')
112
+ assert.match(dsh.aggregatePackage ?? '',/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u,'Exact aggregate package name required')
113
+ if(dsh.aggregateVersion===null && dsh.registry===null)return null
114
+ assert.match(dsh.aggregateVersion ?? '',/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,'Exact aggregate version required')
115
+ assert.equal(typeof dsh.registry,'string','Bound HTTPS registry required')
116
+ const url=new URL(dsh.registry)
117
+ assert.ok(url.protocol==='https:' && !url.username && !url.password && !url.search && !url.hash,'Credential-free HTTPS registry required')
118
+ return {aggregateSelector:`${dsh.aggregatePackage}@${dsh.aggregateVersion}`,registry:url.href}
119
+ }
120
+
121
+ async function boundedFile(path) {
122
+ assert.ok(isAbsolute(path), 'Explicit absolute ownership file required')
123
+ await safeDirectory(dirname(path))
124
+ const info = await lstat(path)
125
+ assert.ok(info.isFile() && info.size <= 1024 * 1024, 'Bounded regular ownership file required')
126
+ return regular(path)
127
+ }
128
+
129
+ /** Pin the acquired product, not an aggregate that a registry operation has yet to acquire. */
130
+ async function productIdentity(root) {
131
+ if (root === undefined) return null
132
+ assert.ok(isAbsolute(root), 'Explicit absolute acquired product required')
133
+ await safeDirectory(root)
134
+ const manifestBytes = await boundedFile(join(root, 'package.json')), manifest = JSON.parse(manifestBytes)
135
+ assert.equal(typeof manifest.name, 'string'); assert.equal(typeof manifest.version, 'string')
136
+ let releaseMap = null
137
+ try { releaseMap = await boundedFile(join(root, 'release-hosts.json')) }
138
+ catch (error) { if (error.code !== 'ENOENT') throw error }
139
+ const entries = {}
140
+ for (const file of ['design-system.mjs', 'installer.mjs', 'npm-installer-dsh.mjs', 'npm-agent-installation.mjs'])
141
+ entries[file] = sha(await boundedFile(join(root, 'bin', file)))
142
+ return { npmRoot: root, name: manifest.name, version: manifest.version, manifestSha256: sha(manifestBytes),
143
+ releaseMapSha256: releaseMap === null ? null : sha(releaseMap),
144
+ releaseSelection: releaseMap === null ? null : validateReleaseHosts(JSON.parse(releaseMap)), entries }
145
+ }
146
+
147
+ function expectedSource(input) {
148
+ return { kind: input.aggregateDirectory ? 'retained-link' : 'registry-selector',
149
+ aggregateSelector: input.aggregateSelector, registry: input.registry,
150
+ ...(input.aggregateDirectory ? { aggregateDirectory: input.aggregateDirectory,
151
+ aggregateManifestSha256: input.aggregateManifestSha256, aggregatePatchSha256: input.aggregatePatchSha256 } : {}) }
152
+ }
153
+ function ownedState(current) {
154
+ return { dependency: current.dependency, installed: current.installed, registered: current.registered }
155
+ }
156
+
157
+ function ownerReference(record) {
158
+ return { receiptFile: record.path, receiptSha256: sha(record.bytes), planSha256: record.plan.planSha256,
159
+ product: record.plan.product, owned: record.receipt.source.observed }
160
+ }
161
+ function assertOwnerReference(reference) {
162
+ assert.ok(reference && typeof reference === 'object', 'Actual native owner reference required')
163
+ assert.ok(typeof reference.receiptFile === 'string' && isAbsolute(reference.receiptFile), 'Actual native owner file required')
164
+ for (const key of ['receiptSha256', 'planSha256']) assert.match(reference[key] ?? '', /^[a-f0-9]{64}$/u, 'Native owner hash required')
165
+ assert.ok(reference.product && reference.owned?.installed && reference.owned.registered, 'Native owner identity required')
166
+ }
167
+
168
+ // This validates a saved fact without reading historical product directories or
169
+ // following its earlier owner. Only the selected current product is reread below.
170
+ async function readOwnedReceipt(path, state, profile, currentProduct) {
171
+ assert.ok(typeof path === 'string' && isAbsolute(path), 'Explicit absolute previous receipt required')
172
+ for (const root of [state.dshHome, currentProduct.npmRoot, ...Object.values(state.pins).map(pin => dirname(pin.path))])
173
+ assert.ok(!beneath(root, path), 'Previous receipt overlaps host or product inputs')
174
+ if (process.platform === 'win32') await assertWindowsPrivate(dirname(path), { directory: true })
175
+ const bytes = await boundedFile(path), receipt = JSON.parse(bytes)
176
+ const { receiptSha256, ...body } = receipt
177
+ assert.equal(receipt.schemaVersion, 2, 'Ownership proof required; sparse or manual receipts cannot be adopted')
178
+ assert.equal(digest(body), receiptSha256, 'Previous receipt digest mismatch')
179
+ assert.equal(receipt.kind, 'dsh-installation'); assert.equal(receipt.host, 'dsh')
180
+ assert.equal(receipt.route, 'official-dsh-bundle'); assert.equal(receipt.scope, state.identity.scope)
181
+ assert.match(receipt.id ?? '', /^[0-9a-f-]{36}$/u)
182
+ assert.equal(receipt.receiptPath, path)
183
+ assert.equal(join(dirname(path), receipt.id + '.dsh-result.json'), path)
184
+ assert.equal(receipt.status, 'registered-config-parsed', 'Successful owned installation required')
185
+ assert.equal(receipt.ownershipEstablished, true, 'Previous installation ownership was not established')
186
+ assert.equal(receipt.exitCode, 0); assert.equal(receipt.partialStateRetained, false)
187
+ assert.equal(receipt.userConfigurationPreserved, true)
188
+ const old = receipt.planSnapshot, { planSha256, ...unsigned } = old ?? {}
189
+ assert.equal(planSha256, receipt.planSha256); assert.equal(digest(unsigned), planSha256, 'Previous plan digest mismatch')
190
+ assert.deepEqual(old.protocol, state.identity, 'Previous official profile or protocol differs')
191
+ assert.equal(old.profile, profile, 'Previous profile differs')
192
+ assert.deepEqual(receipt.product, old.product, 'Previous product identity mismatch')
193
+ assert.ok(old.product?.releaseSelection, 'Previous product was not release-bound')
194
+ assert.ok(!beneath(old.product.npmRoot, path), 'Previous receipt overlaps its acquired product')
195
+ const selected = await selectorInput({ profile: old.profile, aggregateSelector: old.aggregateSelector, registry: old.registry })
196
+ assert.deepEqual(old.package, selected.package, 'Previous selected package differs')
197
+ // A receipt pins both the canonical release origin and the effective download
198
+ // registry. They may differ for an explicitly selected mirror, never in version.
199
+ assert.deepEqual(old.product.releaseSelection, validateReleaseHosts({ schemaVersion: 1, hosts: { dsh: {
200
+ aggregatePackage: selected.package.name, aggregateVersion: selected.package.version,
201
+ registry: old.product.releaseSelection.registry } } }), 'Previous product selection mismatch')
202
+ assert.deepEqual(receipt.source?.expected, expectedSource(old), 'Previous selected source differs')
203
+ assert.deepEqual(receipt.source?.observed, ownedState(receipt.readback), 'Previous installed source differs')
204
+ assert.equal(receipt.readback?.configurationParsed, true)
205
+ assert.equal(receipt.readback?.packageInstalled, true); assert.equal(receipt.readback?.bundleRegistered, true)
206
+ assert.equal(receipt.readback.installed?.name, selected.package.name)
207
+ assert.equal(receipt.readback.installed?.version, selected.package.version)
208
+ assert.equal(receipt.readback.registered, true)
209
+ if (old.aggregateDirectory) {
210
+ assert.equal(receipt.readback.installed.realPackageDir, old.aggregateDirectory)
211
+ assert.ok(receipt.readback.dependency.startsWith('link:'))
212
+ } else assert.equal(receipt.readback.dependency, selected.package.version)
213
+ return { path, bytes, receipt, plan: old }
214
+ }
215
+ function assertNativeOwner(record) {
216
+ const { receipt, plan } = record
217
+ assert.equal(receipt.execution?.state, 'completed', 'Owner must be an actual completed native operation')
218
+ assert.equal(receipt.execution.exitCode, 0, 'Native owner must have exited successfully')
219
+ if (plan.action === 'install') {
220
+ assert.ok(!plan.before.installed && !plan.before.registered && plan.before.dependency === null,
221
+ 'Native install owner must start with an absent aggregate')
222
+ } else {
223
+ assert.equal(plan.action, 'update', 'A no-command observation is not a native owner')
224
+ assertOwnerReference(plan.previous)
225
+ assert.deepEqual(ownedState(plan.before), plan.previous.owned, 'Native update before-state differs from its owner')
226
+ assert.notEqual(plan.before.installed.version, receipt.readback.installed.version)
227
+ assert.notEqual(plan.previous.product.version, plan.product.version)
228
+ assert.equal(plan.previous.product.name, plan.product.name)
229
+ }
230
+ }
231
+
232
+ async function previousOwnership(path, state, input, product) {
233
+ assert.ok(product?.releaseSelection, 'Owned update requires a release-bound acquired product')
234
+ const selected = await readOwnedReceipt(path, state, input.profile, product)
235
+ let anchor = selected
236
+ if (selected.plan.action !== 'install') {
237
+ const reference = selected.plan.previous
238
+ assertOwnerReference(reference)
239
+ // Read exactly one native anchor. Never follow its own earlier reference.
240
+ const native = await readOwnedReceipt(reference.receiptFile, state, input.profile, product)
241
+ assert.deepEqual(ownerReference(native), reference, 'Native owner reference changed')
242
+ assertNativeOwner(native)
243
+ assert.deepEqual(ownedState(selected.plan.before), reference.owned, 'Previous before-state differs from its native owner')
244
+ if (selected.plan.action === 'already-registered') {
245
+ assert.equal(selected.receipt.execution?.state, 'not-needed')
246
+ assert.deepEqual(selected.plan.product, native.plan.product, 'Observation product differs from its native owner')
247
+ assert.deepEqual(selected.receipt.source, native.receipt.source, 'Observation source differs from its native owner')
248
+ anchor = native
249
+ } else assertNativeOwner(selected)
250
+ } else assertNativeOwner(selected)
251
+ assert.deepEqual(await productIdentity(selected.plan.product.npmRoot), selected.plan.product, 'Previous acquired product changed')
252
+ assert.equal(selected.plan.product.name, product.name, 'Previous product name differs')
253
+ const oldInput = await selectorInput(selected.plan)
254
+ assert.equal(oldInput.package.name, input.package.name, 'Previous aggregate name differs')
255
+ assert.equal(selected.plan.product.releaseSelection.registry, product.releaseSelection.registry,
256
+ 'Release registry migration requires a separate route')
257
+ assert.equal(oldInput.registry, input.registry, 'Registry migration requires a separate route')
258
+ assert.equal(Boolean(oldInput.aggregateDirectory), Boolean(input.aggregateDirectory), 'Source-kind migration requires a separate route')
259
+ if (oldInput.package.version === input.package.version)
260
+ assert.deepEqual(product, selected.plan.product, 'Same-version relocation is not an owned update')
261
+ else assert.notEqual(product.version, selected.plan.product.version, 'An update requires a new exact product version')
262
+ return { input: oldInput, inputSha256: sha(selected.bytes), reference: ownerReference(anchor) }
263
+ }
264
+
101
265
  /** Pins are caller-selected release inputs, not host discovery or publication proof. */
102
266
  export async function createDshInstallerAdapter({ scope = 'official-pinned', dshHome,
103
267
  node, cli, hostManifest, loader, loaderManifest, desktopHostManifest, hostVersion, loaderVersion,
@@ -204,6 +368,8 @@ async function snapshot(state, input) {
204
368
  }
205
369
  let installed = null
206
370
  if (packageDir !== null) {
371
+ const realPackageDir = await realpath(packageDir)
372
+ assert.equal(await realpath(join(dir, 'node_modules', name)), realPackageDir, 'Official resolution is not the direct profile package')
207
373
  if (input.aggregateDirectory) assert.equal(await realpath(packageDir), input.aggregateDirectory, 'Resolved aggregate differs from retained installation')
208
374
  const bytes = await regular(join(packageDir, 'package.json')), value = JSON.parse(bytes)
209
375
  assert.equal(value.name, name, 'Resolved package identity conflict')
@@ -214,22 +380,35 @@ async function snapshot(state, input) {
214
380
  const patchPath = resolve(packageDir, patch)
215
381
  assert.ok(beneath(resolve(packageDir), patchPath), 'Aggregate patch escapes package')
216
382
  assert.ok(beneath(await realpath(packageDir), await realpath(patchPath)), 'Aggregate patch resolves outside package')
217
- installed = { name, version: value.version, packageDir, manifestSha256: sha(bytes), patchPath, patchSha256: sha(await regular(patchPath)) }
383
+ installed = { name, version: value.version, packageDir, realPackageDir, manifestSha256: sha(bytes), patchPath, patchSha256: sha(await regular(patchPath)) }
218
384
  } else assert.equal(dependency, undefined, 'Existing aggregate dependency is unresolved; repair separately')
385
+ const lock = await regular(join(dir, 'pnpm-lock.yaml'), true)
219
386
  return { dir, manifestSha256: sha(manifestBytes), foreignSha256: digest(foreignManifest(manifest, name)),
220
- patches, configPins, installed, registered: bundles.includes(name) }
387
+ patches, configPins, installed, dependency: dependency ?? null, bundles: [...bundles],
388
+ lockfileSha256: lock === null ? null : sha(lock), registered: bundles.includes(name) }
221
389
  }
222
390
 
223
391
  /** Read-only plan. Does not initialize profiles, install packages or write configuration. */
224
392
  export async function planDshInstallation(options) {
225
393
  const state = await protocol(options.adapter), input = await selectorInput(options)
226
394
  if (state.pins.desktopHostManifest) assert.equal(input.profile, 'desktop', 'Desktop registration requires its reserved profile')
227
- const before = await snapshot(state, input)
228
- const plan = { schemaVersion: 1, host: 'dsh', route: 'official-dsh-bundle', ...input,
395
+ const product = await productIdentity(options.productRoot ?? options.product?.npmRoot)
396
+ if (product?.releaseSelection) {
397
+ assert.equal(input.aggregateSelector, product.releaseSelection.aggregateSelector, 'Selected aggregate differs from product release')
398
+ // selectorInput already admits only credential-free HTTPS transport. Keep
399
+ // this choice separate from the immutable release origin in product identity.
400
+ }
401
+ const previous = options.previousReceipt === undefined ? null
402
+ : await previousOwnership(options.previousReceipt, state, input, product)
403
+ const before = await snapshot(state, previous?.input ?? input)
404
+ if (previous) assert.deepEqual(ownedState(before), previous.reference.owned, 'Owned A installation changed')
405
+ const plan = { schemaVersion: 2, host: 'dsh', route: 'official-dsh-bundle', ...input, product,
406
+ ...(previous ? { previousReceipt: options.previousReceipt, previousInputSha256: previous.inputSha256, previous: previous.reference } : {}),
229
407
  protocol: structuredClone(state.identity), before, command: state.pins.node.path,
230
408
  // Official profiles contain pnpm-workspace.yaml; explicitly target their root.
231
409
  args: [...state.pins.desktopHostManifest ? ['--expose-internals'] : [], state.pins.cli.path, 'plugin', '--profile', input.profile, 'add', '--workspace-root', '--save-exact', input.aggregateDirectory ? 'link:' + input.aggregateDirectory : input.aggregateSelector, '--registry', input.registry],
232
- action: before.installed && before.registered ? 'already-registered' : 'install',
410
+ action: previous && previous.input.package.version !== input.package.version ? 'update'
411
+ : before.installed && before.registered ? 'already-registered' : 'install',
233
412
  runtimeMounted: false, bundleCycleVerified: false, releaseValidated: false }
234
413
  return { ...plan, planSha256: digest(plan) }
235
414
  }
@@ -271,6 +450,7 @@ export async function inspectDshInstallation(options) {
271
450
 
272
451
  /** Runs only the pinned public CLI; retains partial state on failure, never rolls back. */
273
452
  export async function applyDshInstallation(plan, { adapter, signal, timeoutMs = 0, logDirectory } = {}) {
453
+ plan = structuredClone(plan)
274
454
  const { planSha256, ...unsigned } = plan
275
455
  assert.equal(digest(unsigned), planSha256, 'Plan or profile changed; replan before execution')
276
456
  const state = await protocol(adapter)
@@ -278,14 +458,17 @@ export async function applyDshInstallation(plan, { adapter, signal, timeoutMs =
278
458
  assert.equal(fresh.planSha256, plan.planSha256, 'Plan or profile changed; replan before execution')
279
459
  if (logDirectory !== undefined) {
280
460
  await safeDirectory(logDirectory)
281
- for (const root of [state.dshHome, ...Object.values(state.pins).map(pin => dirname(pin.path))])
461
+ for (const root of [state.dshHome, plan.product?.npmRoot, plan.previous?.product.npmRoot,
462
+ ...Object.values(state.pins).map(pin => dirname(pin.path))].filter(Boolean))
282
463
  assert.ok(!beneath(root, logDirectory) && !beneath(logDirectory, root), 'Command logs overlap host inputs')
283
464
  if (process.platform === 'win32') await assertWindowsPrivate(logDirectory, { directory: true })
284
465
  }
285
466
  let exitCode = 0, executionError = null, execution = { state: 'not-needed', retrySafe: false }, diagnosticError = null
286
467
  if (fresh.action !== 'already-registered') {
287
468
  const command = await executeDshInstall(fresh.command, fresh.args, { cwd: fresh.before.dir,
288
- env: { ...process.env, DSH_HOME: state.dshHome, ...(state.pins.desktopHostManifest ? { ELECTRON_RUN_AS_NODE: '1' } : {}) } },
469
+ // ORT's supported policy skips its extra CUDA download, not dependency lifecycle scripts.
470
+ env: { ...process.env, ONNXRUNTIME_NODE_INSTALL_CUDA: 'skip', DSH_HOME: state.dshHome,
471
+ ...(state.pins.desktopHostManifest ? { ELECTRON_RUN_AS_NODE: '1' } : {}) } },
289
472
  { run: state.run, signal, timeoutMs })
290
473
  ;({ exitCode, executionError, execution } = command)
291
474
  try { await retainCommandOutput(logDirectory, command) }
@@ -296,11 +479,17 @@ export async function applyDshInstallation(plan, { adapter, signal, timeoutMs =
296
479
  readback = await inspectDshInstallation({ ...plan, adapter })
297
480
  userConfigurationPreserved = readback.foreignSha256 === plan.before.foreignSha256
298
481
  && digest(readback.patches) === digest(plan.before.patches) && digest(readback.configPins) === digest(plan.before.configPins)
482
+ && (plan.action !== 'update' || digest(readback.bundles) === digest(plan.before.bundles))
299
483
  } catch (error) { readbackError = error.message }
300
484
  const succeeded = exitCode === 0 && diagnosticError === null && userConfigurationPreserved && readback?.configurationParsed === true
301
- return { scope: adapter.scope, status: succeeded ? 'registered-config-parsed' : 'failed-partial-state-retained',
485
+ const ownershipEstablished = Boolean(succeeded && plan.product?.releaseSelection && (plan.previous
486
+ || plan.action === 'install' && !plan.before.installed && !plan.before.registered && execution.state === 'completed'))
487
+ return { schemaVersion: 2, kind: 'dsh-installation', host: 'dsh', route: 'official-dsh-bundle',
488
+ product: plan.product, planSnapshot: plan, planSha256: plan.planSha256, operation: plan.action,
489
+ ownershipEstablished, source: { expected: expectedSource(plan), observed: readback === null ? null : ownedState(readback) },
490
+ registryAcquisitionVerified: false, scope: adapter.scope, status: succeeded ? 'registered-config-parsed' : 'failed-partial-state-retained',
302
491
  exitCode, executionError, execution, diagnosticError, readbackError, readback, userConfigurationPreserved,
303
492
  userConfigurationStatus: readback === null ? 'unverified' : userConfigurationPreserved ? 'observed-matching' : 'changed',
304
- rollbackAttempted: false, partialStateRetained: !succeeded,
493
+ rollbackAttempted: false, rollbackScope: 'adapter-only; official CLI may perform limited compatibility restoration', partialStateRetained: !succeeded,
305
494
  runtimeMounted: false, bundleCycleVerified: false, releaseValidated: false }
306
495
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yugoo-design/companion",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
4
4
  "description": "Yugoo Design Companion beta: interactive Design Projects and Panels in supported native Agent Hosts.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "yugoo-design",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
4
4
  "description": "Yugoo Design: interactive Design Panels with Claude Code native agents and background design reviews."
5
5
  }