@6reduk/workspace-pipeline 0.4.0 → 0.5.0

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,11 +1,15 @@
1
1
  # Workspace Pipeline CLI
2
2
 
3
+ Claude/Grok shared delivery: [one Claude configuration, plain Grok launch,
4
+ global compatibility preview and recovery](docs/claude-grok.md).
5
+
3
6
  `@6reduk/workspace-pipeline` — workspace-local pipeline delivery. Licensed under MIT.
4
7
 
5
8
  **`--help`, read-only `doctor`, and `logs list/clean/policy` are implemented.**
6
9
  Setup/update/repair/remove/switch/continue use compiled workspace-local adapters
7
- for Codex, Claude, Kimi and Grok. No native plugin installation
8
- or global activation is performed. Claude/Grok shared-entry and skill routing were
10
+ for Codex, Claude, Kimi and legacy native Grok. No native plugin installation
11
+ is performed. Claude-only delivery additionally offers an explicitly approved
12
+ user-wide Grok compatibility prerequisite. Claude/Grok shared-entry and skill routing were
9
13
  observed in disposable sessions. Kimi native verification is deferred; its
10
14
  configuration renderer remains experimental. Real-project migration requires an
11
15
  explicit preview and approval; installation is not blanket runtime certification.
@@ -15,7 +19,8 @@ manifest/source binding changes) and [reset](docs/reset.md) (backed-up restorati
15
19
  or clearing of selected local adapter configuration). Both require a separate
16
20
  preview and explicit apply for changes; these commands are not in registry 0.2.0.
17
21
 
18
- Grok's Claude-import suppression requires the [scoped launch command](docs/launch.md).
22
+ Legacy native Grok's Claude-import suppression uses the [scoped launch command](docs/launch.md).
23
+ New Claude-only delivery uses plain `grok` and deliberately enables those imports.
19
24
  File installation does not establish native session discovery or runtime isolation;
20
25
  Kimi/Grok checks and known generic-skill discovery limits are documented in
21
26
  [native component formats](docs/native-provider-format.md).
@@ -0,0 +1,122 @@
1
+ # One Claude delivery for Claude Code and Grok
2
+
3
+ Select a Claude-only source bundle, such as the new Unity `claude-grok` bundle.
4
+ The name describes its consumers; its provider list contains only `claude`.
5
+ The source supplies one full `CLAUDE.md`, `.claude/skills`, optional
6
+ `.claude/agents`, and `.mcp.json`. Start either `claude` or plain `grok` in the
7
+ wrapper. Do not use the old `launch grok` command: it is only for legacy native
8
+ Grok deliveries and disables the imports this mode needs.
9
+
10
+ ## What setup changes
11
+
12
+ Normal setup/update/repair/reset preview now includes a **separate user-wide
13
+ Grok prerequisite** when the desired delivery includes Claude but not native
14
+ Grok. If Grok is absent, it is reported as `not-present`; Claude still installs.
15
+ Old dual-provider sources retain their legacy behavior until migrated.
16
+
17
+ If Grok exists, explicit apply enables only these settings in its effective user
18
+ profile (`GROK_HOME/config.toml`, otherwise the user's `.grok/config.toml`):
19
+
20
+ ```toml
21
+ [compat.claude]
22
+ skills = true
23
+ rules = true
24
+ agents = true
25
+ mcps = true
26
+ hooks = true
27
+ ```
28
+
29
+ These preferences affect **all Grok workspaces**, including user-installed
30
+ hooks/MCP. They are not workspace isolation. Other settings, including model,
31
+ credentials, trust, permission rules and `compat.claude.sessions`, are preserved.
32
+ The installer never grants folder trust or starts a model/MCP session.
33
+
34
+ Use the normal command with `--json` to save its complete preview, then review
35
+ and apply that exact file. New Claude-only previews bind both the workspace
36
+ plan and the global prerequisite. Old previews must be regenerated; do not
37
+ manually unwrap/edit a preview to avoid its global checks.
38
+ This includes old ordinary (non-switch) continuation previews: regenerate with
39
+ `continue --workspace <path> --recovery <retained-record>` and approve the new
40
+ preview. Existing recovery evidence remains unchanged; it is not a lockout.
41
+
42
+ Close configuration editors while applying. Backups and operation records live
43
+ under `<GROK_HOME>/workspace-pipeline-backups/<id>/`, **not in the workspace**.
44
+ The reported directory contains `config.before.toml` when a file existed,
45
+ `operation.json`, and on success `completed.json`. Backups may contain secrets;
46
+ keep them private. They are not covered by workspace journal cleanup. Review
47
+ and remove obsolete backup directories manually when no longer needed.
48
+
49
+ ## Diagnosis and repair
50
+
51
+ `doctor` checks actual configuration and known overriding environment/policy
52
+ conditions. When a native Grok executable is available it also runs bounded
53
+ `grok inspect --json` without `--trust`, retaining only compatibility results.
54
+ It reports native inspection separately. This does not prove model-visible
55
+ skills, execution of agents, a working MCP connection or task acceptance.
56
+ Untrusted workspace discovery may be incomplete; grant trust yourself in the
57
+ harness only after reviewing the workspace.
58
+
59
+ Conflicting environment overrides, managed policy files, legacy import markers,
60
+ malformed/unsupported TOML, symlinks and stale before hashes fail closed. The CLI
61
+ does not override organization policy or silently remove an import marker.
62
+ An existing policy file conservatively requires separate resolution, even if
63
+ it might not ultimately pin these five fields. A missing native binary yields
64
+ `native: not-available`, not an invented native PASS.
65
+
66
+ If the workspace operation succeeded but global configuration failed, the
67
+ result is `needs-compatibility`, with `workspaceStatus: ready`. Workspace files
68
+ are already installed; run a fresh **repair preview/apply**, rather than replaying
69
+ a stale setup preview. There is no claim of a cross-filesystem atomic transaction.
70
+ Historical switch continuations have no global approval envelope: after workspace
71
+ activation they inspect compatibility without changing it. If needed, finish with
72
+ a fresh repair preview/apply. Source authors must make their complete `CLAUDE.md`
73
+ usable by either harness, not require a nonexistent Grok provider route.
74
+
75
+ To inspect/enable the same prerequisite separately (for example after installing
76
+ Grok later), use:
77
+
78
+ ```powershell
79
+ workspace-pipeline compat claude --json > compat-preview.json
80
+ workspace-pipeline compat claude --apply --preview "C:\absolute\compat-preview.json"
81
+ ```
82
+
83
+ After a killed installer, a recorded dead-process lock can be recovered:
84
+
85
+ ```powershell
86
+ workspace-pipeline compat claude recover-lock --json > lock-preview.json
87
+ workspace-pipeline compat claude recover-lock --apply --preview "C:\absolute\lock-preview.json"
88
+ ```
89
+
90
+ Recovery verifies the saved lock, current config hash and that the owner PID is
91
+ not alive. It removes only that installer lock; it does not restore config or
92
+ delete evidence. Then inspect backups and run fresh repair. A malformed lock,
93
+ PID reuse, or a crash during lock recovery itself requires manual diagnosis;
94
+ the tool refuses to guess or automatically delete an unknown/live lock.
95
+
96
+ ## Update and removal
97
+
98
+ When a still-selected bundle drops its old native Grok member, ordinary update
99
+ retires only owned Grok files/fields and preserves personal settings. Modified
100
+ owned files remain conflicts; review/reset them explicitly. Codex delivery is
101
+ not removed. Grok can also discover `.agents/skills`, so use matching canonical
102
+ pipeline skills and check actual source selection, not merely unique names.
103
+
104
+ Removing a workspace **leaves the user-wide compat preferences enabled**: other
105
+ workspaces may depend on them. Backups are not restored automatically. To undo
106
+ a preference, review the backup and current config and change only the relevant
107
+ fields; never overwrite a later personal config wholesale with an old backup.
108
+
109
+ ## Contributor boundaries
110
+
111
+ `src/compat/claude.js` owns the bounded profile editor/backup/lock and sanitized
112
+ native inspection. `src/compat/lifecycle.js` binds the additional approval at
113
+ the public CLI boundary; workspace schemas, historical recovery bytes and path
114
+ ownership remain unchanged. Embedded `runCli` callers explicitly inject the
115
+ compatibility service; the real executable supplies it. Source packages cannot
116
+ provide executable adapter code or arbitrary global destinations.
117
+
118
+ Run `node --test test/claude-compat.test.js`, the full regression and
119
+ `npm run test:packed:claude-compat`. The packed test uses only temporary profiles
120
+ and exercises a dual-provider-to-Claude-only migration, drift, repair/reset and
121
+ removal. Neither a successful test nor this document claims publication or a
122
+ real-workspace migration.
package/docs/doctor.md CHANGED
@@ -111,8 +111,9 @@ This is installation metadata, not a pipeline/document version requirement.
111
111
  locks, renews approvals or silently selects an updated pipeline version.
112
112
  - A missing state is `not-installed` only when metadata is absent; residual
113
113
  metadata with no valid state remains incomplete, not a clean installation.
114
- - Provider-specific discovery/global compatibility checks are later adapter work;
115
- synthetic provider-double readiness is not live-provider certification.
114
+ - The public CLI additionally checks the [Claude-only Grok prerequisite](claude-grok.md),
115
+ including native compatibility inventory when an executable is available.
116
+ Synthetic provider-double readiness is not live-provider certification.
116
117
 
117
118
  ### Repository completion is not lock recovery
118
119
 
package/docs/launch.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Scoped Grok launch (S11 development)
2
2
 
3
+ For new Claude-only delivery, use **plain `grok`**, not this legacy launcher.
4
+ See [Claude/Grok shared delivery](claude-grok.md). The launcher below is retained
5
+ only for old native-Grok installations and deliberately sets imports false.
6
+
3
7
  ## Configuration limitation (checked 2026-09-23)
4
8
 
5
9
  The desired UX is ordinary `grok` with workspace-local compatibility settings,
@@ -7,8 +11,8 @@ not a mandatory launcher. This is currently blocked by the harness: installed
7
11
  Grok 1.0.40 and the upstream configuration reference list only `mcp_servers`,
8
12
  `plugins`, `permission`, and `mcp.max_output_bytes` as project config inputs.
9
13
  `compat.claude` is not among them. Writing five false values locally must not be
10
- represented as working import suppression. The CLI does not change user-wide
11
- compat settings as a substitute. The legacy launch route below remains an
14
+ represented as working import suppression. This legacy native-Grok route does
15
+ not enable the new user-wide Claude compatibility prerequisite. It remains an
12
16
  explicit workaround, not the approved long-term configuration model.
13
17
 
14
18
  Source: [Grok configuration reference](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/26-config-reference.md),
@@ -80,7 +80,10 @@ restoration; empty reset must be explicitly selected.
80
80
  If stdout fails after a successful apply, exit 2 does not undo that apply.
81
81
  Operation events and `doctor` distinguish actual configuration state from output
82
82
  delivery. Errors expose stable codes, not raw exception messages or argv secrets.
83
- No force unlock, rollback, retry or global configuration mutation is performed.
83
+ The workspace transaction does not force unlock, rollback or mutate global config.
84
+ For Claude-only delivery, the public CLI binds a separate, explicit
85
+ [user-wide Grok prerequisite](claude-grok.md) to preview/apply. Workspace success
86
+ followed by prerequisite failure is reported as `needs-compatibility`.
84
87
 
85
88
  ## Offline repair and removal
86
89
 
@@ -163,4 +166,7 @@ boundary, `uncertain: null` means no ambiguous write: only unrecorded operations
163
166
  and phase checks remain. Completed outcomes are not replayed. After both recorded
164
167
  phase checks, an empty remainder permits separately checked activation, not an
165
168
  inferred phase success. Failed/corrupt journals and changed targets fail closed.
166
- No source download, global setting change, force unlock or retention cleanup occurs.
169
+ No source download, force unlock or retention cleanup occurs. A Claude-only
170
+ continuation with an explicit compatibility envelope can apply its separately
171
+ approved global prerequisite. Historical switch continuations only inspect it
172
+ after activation; missing compatibility requires a fresh repair preview/apply.
@@ -127,6 +127,9 @@ owned routes therefore does not prove that harness can no longer discover a
127
127
  pipeline still installed for another provider. Installer ownership isolation and
128
128
  native discovery are distinct checks.
129
129
 
130
+ The following describes legacy native-Grok delivery, not the new
131
+ [Claude-only compatibility mode](claude-grok.md), which installs no Grok copies.
132
+
130
133
  Grok's child-environment helper sets five Claude compatibility switches to false:
131
134
  skills, rules, agents, MCP and hooks. It changes only the child environment,
132
135
  including removal of conflicting Windows key spellings. It does not switch
package/docs/reset.md CHANGED
@@ -55,7 +55,10 @@ Non-control table entries under `[agents]` are treated as named agent definition
55
55
  Unknown files outside the listed trees are not guessed to be pipeline files.
56
56
  Files under listed trees are included regardless of ignore rules. Empty directories
57
57
  and empty containing configuration files may remain; they are not an active skill
58
- or MCP declaration. Grok still uses its scoped launcher for import suppression.
58
+ or MCP declaration. Legacy native Grok uses its scoped launcher; new Claude-only
59
+ delivery uses [plain Grok and a separate compatibility prerequisite](claude-grok.md).
60
+ Reset-to-installed may explicitly reapply that prerequisite; reset-to-empty and
61
+ removal never disable user-wide compatibility.
59
62
 
60
63
  Malformed configurations, unsupported TOML representations, unsafe paths/links,
61
64
  hardlinks, repository overlap, unavailable snapshots, incompatible renderer replay,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@6reduk/workspace-pipeline",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Workspace-local Git pipeline configuration dispatcher",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "test:packed": "node scripts/verify-packed-cli.mjs",
27
27
  "test:packed:identity": "node scripts/verify-packed-identity.mjs",
28
28
  "test:packed:providers": "node scripts/verify-packed-providers.mjs",
29
+ "test:packed:claude-compat": "node scripts/verify-packed-claude-compat.mjs",
29
30
  "test:packed:pilot": "node scripts/verify-packed-pilot.mjs"
30
31
  },
31
32
  "dependencies": {
package/src/cli.js CHANGED
@@ -2,8 +2,9 @@
2
2
  import {runCli} from './commands/dispatch.js';
3
3
  import {providerRegistry} from './providers/registry.js';
4
4
  import {outputWriter} from './commands/output.js';
5
+ import {createClaudeCompatibility} from './compat/claude.js';
5
6
  const write=stream=>text=>new Promise((resolve,reject)=>stream.write(text,error=>error?reject(error):resolve()));
6
7
  // Prevent an unhandled pipe error; write callbacks report delivery failures.
7
8
  process.stdout.on('error',()=>{});process.stderr.on('error',()=>{});
8
9
  const args=process.argv.slice(2),json=args.includes('--json');
9
- process.exitCode=await runCli(args,{stdout:outputWriter(write(process.stdout),json),stderr:outputWriter(write(process.stderr),json),registry:providerRegistry});
10
+ process.exitCode=await runCli(args,{stdout:outputWriter(write(process.stdout),json),stderr:outputWriter(write(process.stderr),json),registry:providerRegistry,compatibility:createClaudeCompatibility()});
@@ -0,0 +1,22 @@
1
+ import {fail} from '../contracts/parse.js';
2
+ import {absoluteRoot} from '../workspace/paths.js';
3
+ import {contractDigest} from '../contracts/semantic.js';
4
+ import {readRecord} from '../operations/state.js';
5
+ export function parseCompat(args){
6
+ if(args[1]!=='claude')fail('cli.arguments');
7
+ const result={command:'compat',recover:args[2]==='recover-lock'},seen=new Set();
8
+ for(let i=result.recover?3:2;i<args.length;i++){
9
+ const flag=args[i];if(!['--json','--apply','--preview'].includes(flag)||seen.has(flag))fail('cli.arguments');seen.add(flag);
10
+ if(flag==='--json')continue;if(flag==='--apply'){result.apply=true;continue;}
11
+ const value=args[++i];if(!value||value.startsWith('--'))fail('cli.arguments');result.previewFile=absoluteRoot(value);
12
+ }
13
+ if(Boolean(result.apply)!==Boolean(result.previewFile))fail('cli.arguments');return result;
14
+ }
15
+ export async function runCompat(command,service,stdout){
16
+ if(!service)fail('grok-compat.unavailable');
17
+ if(!command.apply){const body={kind:'grok-compat-command',action:command.recover?'recover-lock':'enable',plan:await (command.recover?service.recovery():service.inspect())};
18
+ await stdout(JSON.stringify({...body,digest:contractDigest(body)})+'\n');return body.plan.status==='blocked'?1:0;}
19
+ const record=(await readRecord(command.previewFile)).value,{digest,...body}=record;
20
+ if(Object.keys(record).sort().join(',')!=='action,digest,kind,plan'||record.kind!=='grok-compat-command'||record.action!==(command.recover?'recover-lock':'enable')||digest!==contractDigest(body))fail('grok-compat.envelope');
21
+ const result=await (command.recover?service.recover(record.plan):service.apply(record.plan));await stdout(JSON.stringify(result)+'\n');return 0;
22
+ }
@@ -26,9 +26,12 @@ import {parseMigrationCommand,runMigrationCommand} from './migration.js';
26
26
  import {parseLaunch,runLaunch} from './launch.js';
27
27
  import {parseRebind,runRebind,acceptedRebind} from './rebind.js';
28
28
  import {parseReset,runReset} from './reset.js';
29
+ import {bindCompatibility,unwrapCompatibility,finishCompatibility,doctorCompatibility} from '../compat/lifecycle.js';
30
+ import {parseCompat,runCompat} from './compat.js';
29
31
 
30
32
  export const help=`Workspace Pipeline CLI — development preview
31
33
  Usage: workspace-pipeline doctor --workspace <absolute-directory> [--recovery <relative-record>] [--json]
34
+ workspace-pipeline compat claude [recover-lock] [--apply --preview <absolute-json-file>] [--json]
32
35
  workspace-pipeline launch grok --workspace <absolute-directory> --executable <absolute-native-executable> [--inspect] [--execute]
33
36
  workspace-pipeline <init|adopt|wrap> --workspace <absolute-directory> --choices <absolute-json-file> [--manifest <absolute-file>] [--network]
34
37
  workspace-pipeline <init|adopt|wrap> --workspace <absolute-directory> --apply --preview <absolute-json-file>
@@ -145,6 +148,7 @@ export function parseCommand(args) {
145
148
  if(!Array.isArray(args) || args.some(a=>typeof a!=='string')) fail('cli.arguments');
146
149
  if(args.length===0 || (['--help','-h'].includes(args[0]) && (args.length===1 || (args.length===2 && args[1]==='--json'))))return {command:'help'};
147
150
  if(args[0]==='launch')return parseLaunch(args);
151
+ if(args[0]==='compat')return parseCompat(args);
148
152
  if(args[0]==='rebind')return parseRebind(args);
149
153
  if(args[0]==='reset')return parseReset(args);
150
154
  if(['setup','update','repair','remove','switch','continue'].includes(args[0]))return parseLifecycle(args);
@@ -261,7 +265,7 @@ function parseLifecycle(args) {
261
265
  return result;
262
266
  }
263
267
 
264
- async function runLifecycle(command,registry,stdout,stderr) {
268
+ async function runLifecycle(command,registry,stdout,stderr,compatibility) {
265
269
  // A trusted embedding/CLI assembly can supply functions; argv and package
266
270
  // data cannot. Until real adapters ship, the default command fails before IO.
267
271
  if(registry===null || registry===undefined)fail('cli.providers-unavailable');
@@ -277,12 +281,22 @@ async function runLifecycle(command,registry,stdout,stderr) {
277
281
  if(command.bundles)input.bundles=command.bundles;
278
282
  if(command.recoveryPath)input.recoveryPath=command.recoveryPath;
279
283
  if(command.rebindFile)Object.assign(input,await acceptedRebind(command.workspace,command.rebindFile));
280
- await stdout(JSON.stringify(await prepare(input,registry))+'\n');return 0;
284
+ const prepared=await prepare(input,registry);
285
+ await stdout(JSON.stringify(command.command==='remove'?prepared:await bindCompatibility(prepared,compatibility))+'\n');return 0;
281
286
  }
282
- const prepared=(await readRecord(command.previewFile)).value;
283
- const result=await apply({command:command.command,wrapper:command.workspace,prepared,
287
+ const saved=(await readRecord(command.previewFile)).value;
288
+ const unwrapped=command.command==='remove'?{prepared:saved,compatibility:null}:await unwrapCompatibility(saved,compatibility);
289
+ const prepared=unwrapped.prepared;
290
+ const workspaceResult=await apply({command:command.command,wrapper:command.workspace,prepared,
284
291
  approval:{decision:'approve',preparedDigest:prepared.digest}},registry,
285
292
  {report:async event=>stderr(JSON.stringify(event)+'\n')});
293
+ let result=await finishCompatibility(workspaceResult,unwrapped.compatibility,compatibility);
294
+ // Historical switch continuations do not carry a desired-provider envelope.
295
+ // Inspect the activated installation, but never infer approval for a global write.
296
+ if(command.command==='continue'&&!unwrapped.compatibility&&compatibility&&result.status==='ready'&&result.lockRelease==='released'){
297
+ const checked=await doctorCompatibility(await inspectInstallation(command.workspace),compatibility);
298
+ if(checked.compatibility)result={...result,compatibility:checked.compatibility,...checked.status==='needs-compatibility'?{workspaceStatus:'ready',status:'needs-compatibility'}:{}};
299
+ }
286
300
  await stdout(JSON.stringify(result)+'\n');
287
301
  return (result.status==='ready' || (['remove','continue'].includes(command.command) && result.status==='not-installed')) && result.lockRelease==='released' && !result.outputError?0:1;
288
302
  }
@@ -386,14 +400,15 @@ async function runLogs(command,stdout,stderr) {
386
400
  }finally{await lock.release();}
387
401
  }
388
402
 
389
- export async function runCli(args,{stdout,stderr,registry=null}) {
403
+ export async function runCli(args,{stdout,stderr,registry=null,compatibility=null}) {
390
404
  if(typeof stdout!=='function' || typeof stderr!=='function')fail('cli.transport');
391
405
  try {
392
406
  const command=parseCommand(args);
393
407
  if(command.command==='help') {await stdout(help+'\n');return 0;}
408
+ if(command.command==='compat')return await runCompat(command,compatibility,stdout);
394
409
  if(command.command==='launch')return await runLaunch(command,stdout,stderr);
395
410
  if(command.command==='rebind')return await runRebind(command,stdout);
396
- if(command.command==='reset')return await runReset(command,registry,stdout,stderr);
411
+ if(command.command==='reset')return await runReset(command,registry,stdout,stderr,compatibility);
397
412
  if(command.command==='migration')return await runMigrationCommand(command,stdout,stderr);
398
413
  if(command.command==='logs')return await runLogs(command,stdout,stderr);
399
414
  if(command.command==='policy')return await runPolicy(command,stdout);
@@ -402,9 +417,9 @@ export async function runCli(args,{stdout,stderr,registry=null}) {
402
417
  ['abandon','continue-abandon'].includes(command.action)?runRepositoryAbandon:
403
418
  command.action==='recover-locks'?runRepositoryLockRecovery:
404
419
  ['recover-bootstrap','continue-bootstrap','retire-bootstrap'].includes(command.action)?runBootstrapContinuation:runRepositoryRecovery)(command,stdout);
405
- if(['setup','update','repair','remove','switch','continue'].includes(command.command))return await runLifecycle(command,registry,stdout,stderr);
420
+ if(['setup','update','repair','remove','switch','continue'].includes(command.command))return await runLifecycle(command,registry,stdout,stderr,compatibility);
406
421
  const options=command.recoveryPath===undefined?{}:{recoveryPath:command.recoveryPath};
407
- const result=await inspectInstallation(command.workspace,options);
422
+ const result=await doctorCompatibility(await inspectInstallation(command.workspace,options),compatibility);
408
423
  await stdout(JSON.stringify(result)+'\n');
409
424
  return result.ready?0:1;
410
425
  } catch(error) {
@@ -11,6 +11,11 @@ export function formatResult(value) {
11
11
  if (value.pipeline) lines.push(`Pipeline: ${safe(value.pipeline.id)} @ ${safe(value.pipeline.version)}`,
12
12
  `Providers: ${value.pipeline.providers.map(safe).join(', ')}`);
13
13
  for (const key of ['configuration', 'transactionEvidence']) if (value[key] !== undefined) lines.push(`${key}: ${safe(value[key])}`);
14
+ if(value.compatibility)lines.push(`Grok / Claude compatibility: ${safe(value.compatibility.status)}`,
15
+ ...(value.compatibility.path?[`User config: ${safe(value.compatibility.path)}`]:[]),
16
+ ...(value.compatibility.blockers??[]).map(b=>` - ${safe(b)}`),
17
+ `Native compatibility inspection: ${safe(value.compatibility.native?.status??'not-run')}.`,
18
+ 'Native discovery is not model/agent/MCP execution certification.');
14
19
  lines.push('', `Diagnostics: ${value.diagnostics.length}`);
15
20
  for (const item of value.diagnostics) lines.push(` - ${safe(item.code)}${item.subject ? ': ' + safe(item.subject) : ''}${item.pointer ? ' ' + safe(item.pointer) : ''}`);
16
21
  lines.push('', 'Runtime / model-visible skills / MCP: not verified by doctor.',
@@ -6,6 +6,7 @@ import {applyReset} from '../operations/apply.js';
6
6
  import {acquireWorkspaceLock} from '../operations/lock.js';
7
7
  import {inspectHistory} from '../operations/history.js';
8
8
  import {assertNoRepositoryPending} from '../operations/repository-pending.js';
9
+ import {bindCompatibility,unwrapCompatibility,finishCompatibility} from '../compat/lifecycle.js';
9
10
 
10
11
  export function parseReset(args){
11
12
  const result={command:'reset'},seen=new Set();
@@ -35,12 +36,13 @@ async function history(workspace){
35
36
  await assertNoRepositoryPending(workspace);
36
37
  const h=await inspectHistory(workspace);if(!h.complete || h.diagnostics.length)fail('reset.history');
37
38
  }
38
- export async function runReset(command,registry,stdout,stderr){
39
+ export async function runReset(command,registry,stdout,stderr,compatibility=null){
39
40
  if(!command.apply){
40
41
  await history(command.workspace);
41
- await stdout(JSON.stringify(await prepareReset(command.workspace,registry,command.options))+'\n');return 0;
42
+ await stdout(JSON.stringify(await bindCompatibility(await prepareReset(command.workspace,registry,command.options),compatibility))+'\n');return 0;
42
43
  }
43
- const raw=(await readRecord(command.previewFile)).value;
44
+ const unwrapped=await unwrapCompatibility((await readRecord(command.previewFile)).value,compatibility);
45
+ const raw=unwrapped.prepared;
44
46
  const approval={decision:'approve',preparedDigest:raw.digest};
45
47
  const prepared=validateResetRecord(raw,approval);
46
48
  if(prepared.preview.plan.workspace!==command.workspace)fail('reset.workspace');
@@ -59,6 +61,7 @@ export async function runReset(command,registry,stdout,stderr){
59
61
  result.status=applied.status;
60
62
  }catch(error){result.error=error instanceof ContractError?error.code:'reset.io';}
61
63
  finally{if(lock)try{await lock.release();result.lockRelease='released';}catch{result.lockRelease='failed';}}
62
- await stdout(JSON.stringify(result)+'\n');
63
- return ['ready','not-installed'].includes(result.status) && result.lockRelease==='released'?0:1;
64
+ const final=await finishCompatibility(result,unwrapped.compatibility,compatibility);
65
+ await stdout(JSON.stringify(final)+'\n');
66
+ return ['ready','not-installed'].includes(final.status) && final.lockRelease==='released'?0:1;
64
67
  }
@@ -0,0 +1,152 @@
1
+ // Compiled user-profile policy, outside workspace ownership. Packages cannot
2
+ // choose global paths or arbitrary keys. No harness, model or MCP is launched.
3
+ import path from 'node:path';
4
+ import {homedir} from 'node:os';
5
+ import {lstat,readFile,mkdir,rename,unlink,open} from 'node:fs/promises';
6
+ import {randomUUID} from 'node:crypto';
7
+ import {isDeepStrictEqual} from 'node:util';
8
+ import {promisify} from 'node:util';
9
+ import {execFile} from 'node:child_process';
10
+ import {parseTOML,getStaticTOMLValue} from 'toml-eslint-parser';
11
+ import {fail} from '../contracts/parse.js';
12
+ import {contractDigest} from '../contracts/semantic.js';
13
+ import {sha256,utf8} from '../source/inventory.js';
14
+ export const compatKeys=Object.freeze(['skills','rules','agents','mcps','hooks']);
15
+ const keys=node=>node.keys.map(k=>k.name??k.value);
16
+ const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v)&&!(v instanceof Date);
17
+ function document(bytes){
18
+ if(bytes!==null&&(!Buffer.isBuffer(bytes)||bytes.length>1024*1024))fail('grok-compat.size');
19
+ const text=bytes===null?'':utf8(bytes);
20
+ let ast,value;try{ast=parseTOML(text,{tomlVersion:'1.0'});value=getStaticTOMLValue(ast);}catch{fail('grok-compat.syntax');}
21
+ if(value.compat!==undefined&&!object(value.compat))fail('grok-compat.shape');
22
+ if(value.compat?.claude!==undefined&&!object(value.compat.claude))fail('grok-compat.shape');
23
+ if(value.claude_compat?.imported===true)fail('grok-compat.import-marker');
24
+ for(const k of compatKeys)if(value.compat?.claude?.[k]!==undefined&&typeof value.compat.claude[k]!=='boolean')fail('grok-compat.shape');
25
+ return {text,ast,value};
26
+ }
27
+ export function enableClaudeCompat(bytes){
28
+ const {text,ast,value}=document(bytes),edits=[],seen=new Set();
29
+ const changes=compatKeys.filter(k=>value.compat?.claude?.[k]!==true);
30
+ if(!changes.length)return {bytes:bytes??Buffer.alloc(0),changes};
31
+ let table=null,parentTable=null;
32
+ function pair(node,base){
33
+ const p=[...base,...keys(node.key)];if(p[0]!=='compat')return;
34
+ if(p.length===1||(p.length===2&&p[1]==='claude'))fail('grok-compat.inline-ancestor');
35
+ if(p.length===3&&p[1]==='claude'&&compatKeys.includes(p[2])){
36
+ seen.add(p[2]);if(changes.includes(p[2]))edits.push([...node.value.range,'true']);
37
+ }
38
+ }
39
+ for(const node of ast.body[0].body){
40
+ if(node.type==='TOMLKeyValue')pair(node,[]);
41
+ else if(node.type==='TOMLTable'){
42
+ if(node.resolvedKey.join('.')==='compat.claude'){if(node.kind!=='standard')fail('grok-compat.shape');table=node;}
43
+ if(node.resolvedKey.length===1&&node.resolvedKey[0]==='compat'){if(node.kind!=='standard')fail('grok-compat.shape');parentTable=node;}
44
+ for(const item of node.body)pair(item,node.resolvedKey);
45
+ }
46
+ }
47
+ const missing=compatKeys.filter(k=>!seen.has(k)),nl=text.includes('\r\n')?'\r\n':'\n';
48
+ if(missing.length){
49
+ const selected=table??parentTable;
50
+ if(selected){const end=text.indexOf('\n',selected.key.range[1]),pos=end<0?text.length:end+1;
51
+ edits.push([pos,pos,(end<0?nl:'')+missing.map(k=>`${table?'':'claude.'}${k} = true${nl}`).join('')]);
52
+ }else edits.push([0,0,missing.map(k=>`compat.claude.${k} = true${nl}`).join('')]);
53
+ }
54
+ let result=text;for(const [start,end,replacement]of edits.sort((a,b)=>b[0]-a[0]))result=result.slice(0,start)+replacement+result.slice(end);
55
+ const expected=structuredClone(value);expected.compat??={};expected.compat.claude??={};for(const k of compatKeys)expected.compat.claude[k]=true;
56
+ // The strict decoder consumes a leading UTF-8 BOM; preserve it on re-encode.
57
+ const bom=bytes?.subarray(0,3).equals(Buffer.from([0xef,0xbb,0xbf]));
58
+ const output=Buffer.from((bom?'\uFEFF':'')+result);if(!isDeepStrictEqual(document(output).value,expected))fail('grok-compat.postcondition');
59
+ return {bytes:output,changes};
60
+ }
61
+ async function stat(p){try{return await lstat(p);}catch(e){if(e.code==='ENOENT')return null;throw e;}}
62
+ async function safe(p){
63
+ let current=path.parse(p).root;
64
+ for(const segment of path.relative(current,p).split(path.sep)){
65
+ current=path.join(current,segment);const s=await stat(current);
66
+ if(s&&(s.isSymbolicLink()||(!s.isDirectory()&&current!==p)))fail('grok-compat.path');
67
+ }
68
+ }
69
+ async function read(p){
70
+ await safe(p);const s=await stat(p);if(!s)return null;
71
+ if(!s.isFile()||s.nlink!==1||s.size>1024*1024)fail('grok-compat.file');
72
+ const bytes=await readFile(p),after=await lstat(p);
73
+ if(after.ino!==s.ino||after.dev!==s.dev||after.size!==bytes.length||after.mtimeMs!==s.mtimeMs)fail('grok-compat.drift');
74
+ return bytes;
75
+ }
76
+ const digest=b=>b===null?null:sha256(b);
77
+ async function record(p,value){const h=await open(p,'wx',0o600);try{await h.writeFile(JSON.stringify(value,null,2)+'\n');await h.sync();}finally{await h.close();}}
78
+ const warning='User-wide Claude compatibility affects every Grok workspace, including user hooks/MCP. Removing one workspace does not disable it.';
79
+ export function createClaudeCompatibility({env=process.env,home=homedir()}={}){
80
+ function profile(){const base=env.GROK_HOME||path.join(env.USERPROFILE||env.HOME||home,'.grok');if(!path.isAbsolute(base))fail('grok-compat.home');return path.resolve(base);}
81
+ async function executable(){
82
+ const filename=process.platform==='win32'?'grok.exe':'grok';
83
+ const candidates=[path.join(env.USERPROFILE||env.HOME||home,'.grok','bin',filename),...(env.PATH||'').split(path.delimiter).filter(p=>path.isAbsolute(p)).map(p=>path.join(p,filename))];
84
+ for(const p of candidates)if((await stat(p))?.isFile())return p;return null;
85
+ }
86
+ async function inspect(){
87
+ const base=profile(),target=path.join(base,'config.toml');await safe(base);
88
+ if(!await stat(base)&&!await executable())return {kind:'grok-claude-prerequisite',profile:base,path:target,status:'not-present',beforeHash:null,resultHash:null,changes:[],blockers:[],warning};
89
+ const before=await read(target),edited=enableClaudeCompat(before),blockers=[];
90
+ for(const key of compatKeys){const name=`GROK_CLAUDE_${key.toUpperCase()}_ENABLED`;if(Object.keys(env).some(k=>k.toUpperCase()===name&&!['1','true'].includes(String(env[k]).toLowerCase())))blockers.push('environment:'+name);}
91
+ for(const name of ['managed_config.toml','requirements.toml'])if(await stat(path.join(base,name)))blockers.push('policy:'+name);
92
+ if(await stat(path.join(base,'workspace-pipeline-compat.lock')))blockers.push('global-lock');
93
+ if(await stat(path.join(base,'workspace-pipeline-compat-recovery.lock')))blockers.push('global-recovery-lock');
94
+ return {kind:'grok-claude-prerequisite',profile:base,path:target,status:blockers.length?'blocked':edited.changes.length?'needs-apply':'configured',beforeHash:digest(before),resultHash:digest(edited.bytes),changes:edited.changes,blockers,warning,verification:'config-only; native effective state and runtime not verified'};
95
+ }
96
+ async function check(expected){const actual=await inspect();if(contractDigest(actual)!==contractDigest(expected))fail('grok-compat.preview-drift');if(actual.status==='blocked')fail('grok-compat.blocked');return actual;}
97
+ async function apply(expected){
98
+ const plan=await check(expected);if(plan.status!=='needs-apply')return {...plan,changed:false};
99
+ const base=profile(),lockPath=path.join(base,'workspace-pipeline-compat.lock');await safe(base);await mkdir(base,{recursive:true,mode:0o700});await safe(base);
100
+ const token=randomUUID(),lock=await open(lockPath,'wx',0o600);let temporary,backupDir,lockBytes;
101
+ try{
102
+ lockBytes=Buffer.from(JSON.stringify({pid:process.pid,token,createdAt:new Date().toISOString()})+'\n');await lock.writeFile(lockBytes);await lock.sync();
103
+ if(await stat(path.join(base,'workspace-pipeline-compat-recovery.lock')))fail('grok-compat.recovery-active');
104
+ const before=await read(plan.path);if(digest(before)!==plan.beforeHash)fail('grok-compat.preview-drift');
105
+ const output=enableClaudeCompat(before).bytes;if(digest(output)!==plan.resultHash)fail('grok-compat.preview-drift');
106
+ backupDir=path.join(base,'workspace-pipeline-backups',token);await safe(backupDir);await mkdir(backupDir,{recursive:true,mode:0o700});
107
+ if(before!==null){const b=await open(path.join(backupDir,'config.before.toml'),'wx',0o600);try{await b.writeFile(before);await b.sync();}finally{await b.close();}}
108
+ if(before!==null&&digest(await read(path.join(backupDir,'config.before.toml')))!==plan.beforeHash)fail('grok-compat.backup');
109
+ await record(path.join(backupDir,'operation.json'),{schemaVersion:1,kind:'grok-claude-compatibility',plan,pid:process.pid,token,scope:compatKeys,restoration:'Explicit only; never overwrite later user edits'});
110
+ temporary=path.join(base,`workspace-pipeline-${token}.tmp`);const h=await open(temporary,'wx',0o600);try{await h.writeFile(output);await h.sync();}finally{await h.close();}
111
+ if(digest(await read(plan.path))!==plan.beforeHash)fail('grok-compat.preview-drift');await rename(temporary,plan.path);temporary=null;
112
+ if(digest(await read(plan.path))!==plan.resultHash)fail('grok-compat.readback');
113
+ await record(path.join(backupDir,'completed.json'),{resultHash:plan.resultHash});
114
+ return {...plan,status:'configured',changed:true,backupDir};
115
+ }finally{await lock.close();if(temporary)await unlink(temporary).catch(()=>{});if(lockBytes&&(await read(lockPath))?.equals(lockBytes))await unlink(lockPath);}
116
+ }
117
+ async function recovery(){
118
+ const base=profile(),lockPath=path.join(base,'workspace-pipeline-compat.lock'),bytes=await read(lockPath);
119
+ if(bytes===null)fail('grok-compat.no-lock');
120
+ let owner;try{owner=JSON.parse(utf8(bytes));}catch{fail('grok-compat.lock-record');}
121
+ if(Object.keys(owner).sort().join(',')!=='createdAt,pid,token'||!Number.isSafeInteger(owner.pid)||owner.pid<1||
122
+ !/^[a-f0-9-]{36}$/.test(owner.token)||typeof owner.createdAt!=='string')fail('grok-compat.lock-record');
123
+ try{process.kill(owner.pid,0);fail('grok-compat.owner-running');}catch(e){if(e.code!=='ESRCH')throw e;}
124
+ if(await stat(path.join(base,'workspace-pipeline-compat-recovery.lock')))fail('grok-compat.recovery-active');
125
+ return {kind:'grok-compat-lock-recovery',profile:base,path:lockPath,lockHash:sha256(bytes),owner,
126
+ configHash:digest(await read(path.join(base,'config.toml'))),backupDir:path.join(base,'workspace-pipeline-backups',owner.token),
127
+ warning:'Removes only this dead-process installer lock. Does not restore or rewrite config, backups or workspace.'};
128
+ }
129
+ async function recover(expected){
130
+ const actual=await recovery();if(contractDigest(actual)!==contractDigest(expected))fail('grok-compat.preview-drift');
131
+ const guardPath=path.join(profile(),'workspace-pipeline-compat-recovery.lock'),guard=await open(guardPath,'wx',0o600);
132
+ const guardBytes=Buffer.from(JSON.stringify({pid:process.pid,token:randomUUID()}));
133
+ try{
134
+ await guard.writeFile(guardBytes);await guard.sync();
135
+ const bytes=await read(actual.path);if(digest(bytes)!==actual.lockHash||digest(await read(path.join(profile(),'config.toml')))!==actual.configHash)fail('grok-compat.preview-drift');
136
+ try{process.kill(actual.owner.pid,0);fail('grok-compat.owner-running');}catch(e){if(e.code!=='ESRCH')throw e;}
137
+ await unlink(actual.path);return {...actual,status:'lock-released',next:'Run repair preview/apply; preserve backups and inspect whether the prior config write completed.'};
138
+ }finally{await guard.close();if((await read(guardPath))?.equals(guardBytes))await unlink(guardPath);}
139
+ }
140
+ async function verify(workspace){
141
+ const binary=await executable();if(!binary)return {status:'not-available',runtime:'not-run'};
142
+ if(!path.isAbsolute(workspace))fail('grok-compat.workspace');
143
+ try{
144
+ const {stdout}=await promisify(execFile)(binary,['inspect','--json'],{cwd:workspace,env,windowsHide:true,encoding:'utf8',timeout:30000,maxBuffer:8*1024*1024});
145
+ const data=JSON.parse(stdout),cells=data.externalCompat?.cells;
146
+ const effective=compatKeys.map(surface=>({surface,enabled:Array.isArray(cells)&&cells.filter(c=>c.vendor==='claude'&&c.surface===surface).length===1&&cells.find(c=>c.vendor==='claude'&&c.surface===surface).enabled===true}));
147
+ return {status:effective.every(c=>c.enabled)?'verified':'blocked',effective,projectTrusted:data.projectTrusted===true,
148
+ runtime:'not-run',method:'native inspect only; no trust grant, model or MCP invocation'};
149
+ }catch{return {status:'blocked',code:'grok-compat.native-inspect',runtime:'not-run'};}
150
+ }
151
+ return Object.freeze({inspect,check,apply,recovery,recover,verify});
152
+ }
@@ -0,0 +1,45 @@
1
+ import {contractDigest} from '../contracts/semantic.js';
2
+ import {fail,ContractError,parse} from '../contracts/parse.js';
3
+ export const usesClaudeCompatibility=providers=>providers?.includes('claude')&&!providers.includes('grok');
4
+ const selected=p=>usesClaudeCompatibility(p?.preview?.plan?.desired?.providers??
5
+ (p?.kind==='prepared-switch'?p.preview?.phases?.find(s=>s.name==='install-new')?.preview?.plan?.desired?.providers:undefined));
6
+ export async function bindCompatibility(prepared,service){
7
+ if(!service||!selected(prepared))return prepared;
8
+ const body={kind:'prepared-with-claude-compatibility',workspace:prepared,compatibility:await service.inspect()};
9
+ const result={...body,digest:contractDigest(body)};
10
+ parse(JSON.stringify(result),'json'); // Still readable by the public preview reader.
11
+ return result;
12
+ }
13
+ export async function unwrapCompatibility(prepared,service){
14
+ if(prepared?.kind!=='prepared-with-claude-compatibility'){
15
+ if(service&&selected(prepared))fail('grok-compat.preview-required');
16
+ return {prepared,compatibility:null};
17
+ }
18
+ if(!service||Object.keys(prepared).sort().join(',')!=='compatibility,digest,kind,workspace'||!selected(prepared.workspace))fail('grok-compat.envelope');
19
+ const {digest,...body}=prepared;if(digest!==contractDigest(body))fail('grok-compat.envelope');
20
+ await service.check(prepared.compatibility);
21
+ return {prepared:prepared.workspace,compatibility:prepared.compatibility};
22
+ }
23
+ export async function finishCompatibility(result,requirement,service){
24
+ if(!requirement)return result;
25
+ if(result.status!=='ready'||result.lockRelease!=='released'||result.outputError)return {...result,compatibility:{status:'not-applied',reason:'workspace-operation-incomplete'}};
26
+ try{
27
+ const compatibility=await service.apply(requirement);
28
+ if(compatibility.status==='configured'&&service.verify&&result.workspace){
29
+ compatibility.native=await service.verify(result.workspace);
30
+ if(compatibility.native.status==='blocked')return {...result,workspaceStatus:result.status,status:'needs-compatibility',compatibility};
31
+ }
32
+ return {...result,compatibility};
33
+ }
34
+ catch(error){return {...result,workspaceStatus:result.status,status:'needs-compatibility',compatibility:{status:'failed',profile:requirement.profile,error:error instanceof ContractError?error.code:'grok-compat.io',next:'Run repair preview/apply; workspace changes are already committed. Inspect workspace-pipeline-backups and the global lock in this profile before retry after a process crash.'}};}
35
+ }
36
+ export async function doctorCompatibility(result,service){
37
+ if(!service||!usesClaudeCompatibility(result.pipeline?.providers))return result;
38
+ try{
39
+ const compatibility=await service.inspect();
40
+ if(compatibility.status==='configured'&&service.verify&&result.workspace)compatibility.native=await service.verify(result.workspace);
41
+ const okay=['configured','not-present'].includes(compatibility.status)&&compatibility.native?.status!=='blocked';
42
+ return {...result,compatibility,ready:result.ready&&okay,status:okay?result.status:'needs-compatibility',
43
+ diagnostics:[...result.diagnostics,...okay?[]:[{code:compatibility.native?.status==='blocked'?'grok-compat.native-mismatch':'grok-compat.'+compatibility.status,subject:compatibility.path}]]};
44
+ }catch(error){return {...result,ready:false,status:'needs-compatibility',compatibility:{status:'blocked'},diagnostics:[...result.diagnostics,{code:error instanceof ContractError?error.code:'grok-compat.io',subject:'Grok user profile'}]};}
45
+ }