@6reduk/workspace-pipeline 0.3.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 +11 -4
- package/docs/claude-grok.md +122 -0
- package/docs/doctor.md +5 -3
- package/docs/launch.md +18 -0
- package/docs/lifecycle-cli.md +11 -4
- package/docs/native-provider-format.md +3 -0
- package/docs/rebind.md +2 -2
- package/docs/repositories.md +1 -1
- package/docs/repository-recovery.md +2 -2
- package/docs/reset.md +5 -2
- package/docs/retention.md +2 -1
- package/package.json +2 -1
- package/src/cli.js +4 -1
- package/src/commands/compat.js +22 -0
- package/src/commands/dispatch.js +28 -11
- package/src/commands/launch.js +2 -1
- package/src/commands/output.js +51 -0
- package/src/commands/rebind.js +2 -1
- package/src/commands/reset.js +10 -6
- package/src/compat/claude.js +152 -0
- package/src/compat/lifecycle.js +45 -0
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
|
-
|
|
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
|
|
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).
|
|
@@ -52,7 +57,9 @@ npm run pack:check
|
|
|
52
57
|
npm run test:packed:providers
|
|
53
58
|
```
|
|
54
59
|
|
|
55
|
-
The executable name is `workspace-pipeline`.
|
|
60
|
+
The executable name is `workspace-pipeline`. Output is readable by default;
|
|
61
|
+
add `--json` for complete machine output. Use `--json` when saving any preview
|
|
62
|
+
for a later apply, even when redirecting stdout to a file. Doctor exit 0 means
|
|
56
63
|
observed configuration ready, 1 means not ready/incomplete, 2 means invocation or
|
|
57
64
|
transport error. Unsupported commands exit with code 2. On Windows use an absolute
|
|
58
65
|
path such as `C:\Projects\my-workspace`. See [doctor](docs/doctor.md) for limits.
|
|
@@ -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
|
@@ -5,7 +5,8 @@ The public CLI exposes it read-only:
|
|
|
5
5
|
|
|
6
6
|
`workspace-pipeline doctor --workspace <absolute-path> [--recovery <relative-record>] [--json]`
|
|
7
7
|
|
|
8
|
-
Output is
|
|
8
|
+
Output is a readable status summary by default. `--json` returns the complete
|
|
9
|
+
machine report (including every target/hash). Exit 0 means configuration ready, 1 not ready
|
|
9
10
|
or incomplete, 2 invalid invocation or output failure. This is not an install command.
|
|
10
11
|
|
|
11
12
|
`ready` means observed **configuration readiness**, never a running harness,
|
|
@@ -110,8 +111,9 @@ This is installation metadata, not a pipeline/document version requirement.
|
|
|
110
111
|
locks, renews approvals or silently selects an updated pipeline version.
|
|
111
112
|
- A missing state is `not-installed` only when metadata is absent; residual
|
|
112
113
|
metadata with no valid state remains incomplete, not a clean installation.
|
|
113
|
-
-
|
|
114
|
-
|
|
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.
|
|
115
117
|
|
|
116
118
|
### Repository completion is not lock recovery
|
|
117
119
|
|
package/docs/launch.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
7
|
+
## Configuration limitation (checked 2026-09-23)
|
|
8
|
+
|
|
9
|
+
The desired UX is ordinary `grok` with workspace-local compatibility settings,
|
|
10
|
+
not a mandatory launcher. This is currently blocked by the harness: installed
|
|
11
|
+
Grok 1.0.40 and the upstream configuration reference list only `mcp_servers`,
|
|
12
|
+
`plugins`, `permission`, and `mcp.max_output_bytes` as project config inputs.
|
|
13
|
+
`compat.claude` is not among them. Writing five false values locally must not be
|
|
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
|
|
16
|
+
explicit workaround, not the approved long-term configuration model.
|
|
17
|
+
|
|
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),
|
|
19
|
+
configuration layers and `compat` fields. No new live-isolation claim is made.
|
|
20
|
+
|
|
3
21
|
This command starts Grok from an already configured workspace with five Claude
|
|
4
22
|
compatibility switches set to false in the child environment: skills, rules, agents,
|
|
5
23
|
MCP and hooks. It does not install a pipeline. Configure Grok using the ordinary
|
package/docs/lifecycle-cli.md
CHANGED
|
@@ -23,7 +23,8 @@ workspace-pipeline setup --workspace <absolute-wrapper> --apply --preview <absol
|
|
|
23
23
|
workspace-pipeline update --workspace <absolute-wrapper> --apply --preview <absolute-json-file>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Preview
|
|
26
|
+
Preview displays a human-readable summary by default. Add `--json` to return
|
|
27
|
+
the complete prepared JSON on stdout for saving and subsequent apply. It may acquire a Git
|
|
27
28
|
source into temporary storage outside the wrapper, but does not write provider
|
|
28
29
|
configuration. Network acquisition requires explicit `--network`. When manifest
|
|
29
30
|
is omitted, setup uses the standard workspace manifest; update uses the recorded
|
|
@@ -68,7 +69,7 @@ restoration; empty reset must be explicitly selected.
|
|
|
68
69
|
|
|
69
70
|
## Output and failures
|
|
70
71
|
|
|
71
|
-
- stdout: one prepared JSON or operation result.
|
|
72
|
+
- stdout: readable result by default; with `--json`, one complete prepared JSON or operation result.
|
|
72
73
|
- stderr during apply: structured journal/recovery locations and operation events.
|
|
73
74
|
- exit 0: preview created, or observed configuration ready with successful lock
|
|
74
75
|
release and reporting. It never certifies harness/MCP runtime.
|
|
@@ -79,7 +80,10 @@ restoration; empty reset must be explicitly selected.
|
|
|
79
80
|
If stdout fails after a successful apply, exit 2 does not undo that apply.
|
|
80
81
|
Operation events and `doctor` distinguish actual configuration state from output
|
|
81
82
|
delivery. Errors expose stable codes, not raw exception messages or argv secrets.
|
|
82
|
-
|
|
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`.
|
|
83
87
|
|
|
84
88
|
## Offline repair and removal
|
|
85
89
|
|
|
@@ -162,4 +166,7 @@ boundary, `uncertain: null` means no ambiguous write: only unrecorded operations
|
|
|
162
166
|
and phase checks remain. Completed outcomes are not replayed. After both recorded
|
|
163
167
|
phase checks, an empty remainder permits separately checked activation, not an
|
|
164
168
|
inferred phase success. Failed/corrupt journals and changed targets fail closed.
|
|
165
|
-
No source download,
|
|
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/rebind.md
CHANGED
|
@@ -15,8 +15,8 @@ The following PowerShell example uses illustrative absolute paths; replace them.
|
|
|
15
15
|
Use a private directory for preview files: they can contain configuration values.
|
|
16
16
|
|
|
17
17
|
```powershell
|
|
18
|
-
workspace-pipeline rebind --workspace 'C:\Work\Game' --manifest 'C:\Work\Game\workspace.json' > 'C:\Private\rebind.json'
|
|
19
|
-
workspace-pipeline update --workspace 'C:\Work\Game' --accept-rebind 'C:\Private\rebind.json' > 'C:\Private\update.json'
|
|
18
|
+
workspace-pipeline rebind --workspace 'C:\Work\Game' --manifest 'C:\Work\Game\workspace.json' --json > 'C:\Private\rebind.json'
|
|
19
|
+
workspace-pipeline update --workspace 'C:\Work\Game' --accept-rebind 'C:\Private\rebind.json' --json > 'C:\Private\update.json'
|
|
20
20
|
workspace-pipeline update --workspace 'C:\Work\Game' --apply --preview 'C:\Private\update.json'
|
|
21
21
|
workspace-pipeline doctor --workspace 'C:\Work\Game'
|
|
22
22
|
```
|
package/docs/repositories.md
CHANGED
|
@@ -27,7 +27,7 @@ commit, not uncommitted files of its source.
|
|
|
27
27
|
workspace-pipeline wrap --workspace "C:\Work\GameWorkspace" --manifest "C:\Work\workspace.json" --choices "C:\Work\choices.json"
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Use `--json` and save the exact JSON stdout as UTF-8 `preview.json` outside the affected repository
|
|
31
31
|
and wrapper. Read its operations and blockers. Preview may acquire a temporary
|
|
32
32
|
Git snapshot, but does not perform repository effects. Remote access is explicit
|
|
33
33
|
with `--network`; local Git does not need that flag. Do not put credentials into
|
|
@@ -39,8 +39,8 @@ delete pending markers or lock directories to make `doctor` report success.
|
|
|
39
39
|
|
|
40
40
|
`no-pending-marker` means exactly that; it does not certify historical completion,
|
|
41
41
|
provider readiness or game runtime. A blocker means further recovery is needed;
|
|
42
|
-
this command never grants itself authority to resolve it. `--json`
|
|
43
|
-
|
|
42
|
+
this command never grants itself authority to resolve it. Add `--json` for
|
|
43
|
+
machine-readable output and whenever saving a preview for apply.
|
|
44
44
|
|
|
45
45
|
## Local recovery ownership
|
|
46
46
|
|
package/docs/reset.md
CHANGED
|
@@ -13,7 +13,7 @@ Close running harnesses/configuration editors. Keep preview files private: they
|
|
|
13
13
|
contain before/after bytes, potentially including MCP credentials.
|
|
14
14
|
|
|
15
15
|
```powershell
|
|
16
|
-
workspace-pipeline reset --workspace 'C:\Work\Game' --all > 'C:\Private\reset.json'
|
|
16
|
+
workspace-pipeline reset --workspace 'C:\Work\Game' --all --json > 'C:\Private\reset.json'
|
|
17
17
|
# Check $LASTEXITCODE and review reset.scope, reset.backup and preview.plan.targets.
|
|
18
18
|
workspace-pipeline reset --workspace 'C:\Work\Game' --apply --preview 'C:\Private\reset.json'
|
|
19
19
|
workspace-pipeline doctor --workspace 'C:\Work\Game'
|
|
@@ -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
|
|
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/docs/retention.md
CHANGED
|
@@ -75,7 +75,8 @@ check has cost proportional to selected files times retained records, bounded by
|
|
|
75
75
|
scan limits and the user deletion cap. It is not a linear-cost guarantee or an
|
|
76
76
|
OS-wide defense against hostile concurrent filesystem writers.
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
With `--json`, progress is emitted as JSON on stderr and final JSON on stdout;
|
|
79
|
+
otherwise both are formatted for reading. Save apply previews using `--json`. The receipt path
|
|
79
80
|
is under `.pipeline/cleanup/<run-id>.json`. Receipts record selected file hashes,
|
|
80
81
|
confirmed deletions, reclaimed bytes, current intent and failures. These receipts
|
|
81
82
|
describe attempted/deleted paths; unlike operational recovery records, they are
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@6reduk/workspace-pipeline",
|
|
3
|
-
"version": "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
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {runCli} from './commands/dispatch.js';
|
|
3
3
|
import {providerRegistry} from './providers/registry.js';
|
|
4
|
+
import {outputWriter} from './commands/output.js';
|
|
5
|
+
import {createClaudeCompatibility} from './compat/claude.js';
|
|
4
6
|
const write=stream=>text=>new Promise((resolve,reject)=>stream.write(text,error=>error?reject(error):resolve()));
|
|
5
7
|
// Prevent an unhandled pipe error; write callbacks report delivery failures.
|
|
6
8
|
process.stdout.on('error',()=>{});process.stderr.on('error',()=>{});
|
|
7
|
-
|
|
9
|
+
const args=process.argv.slice(2),json=args.includes('--json');
|
|
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
|
+
}
|
package/src/commands/dispatch.js
CHANGED
|
@@ -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>
|
|
@@ -62,7 +65,9 @@ Preview stages Git externally; inspect is read-only/offline. JSON can contain
|
|
|
62
65
|
private config bytes. Only explicit apply writes or clears a validated pending marker.
|
|
63
66
|
Apply requires the saved installer-bound envelope and accepts no source overrides.
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
Output is human-readable by default; use --json for complete machine output.
|
|
69
|
+
Save previews for apply with --json (including when redirecting stdout).
|
|
70
|
+
Doctor is read-only. Exit 0 means observed configuration ready;
|
|
66
71
|
exit 1 means not ready/incomplete; exit 2 means invalid invocation or unavailable command.
|
|
67
72
|
Runtime, MCP/harness discovery and provider compatibility are NOT verified.
|
|
68
73
|
No source access, automatic repair or lock removal. Read-only/configuration commands never clean history.
|
|
@@ -134,15 +139,16 @@ logs clean --repositories --workspace <absolute-directory> --max-age-days <N>
|
|
|
134
139
|
Apply with --repositories --apply --preview <file>; this is a separate cleanup
|
|
135
140
|
domain, never an implicit increase of the ordinary journal deletion budget.
|
|
136
141
|
Execution inputs are retained privately under .pipeline/repository-inputs;
|
|
137
|
-
--json
|
|
142
|
+
--json selects the complete machine-readable result without presentation changes.
|
|
138
143
|
This package is not ready to replace an existing installation.`;
|
|
139
144
|
|
|
140
145
|
// Parse strictly before any filesystem observation. Never echo unknown arguments
|
|
141
146
|
// (which can contain credentials). Output transport is trusted CLI code.
|
|
142
147
|
export function parseCommand(args) {
|
|
143
148
|
if(!Array.isArray(args) || args.some(a=>typeof a!=='string')) fail('cli.arguments');
|
|
144
|
-
if(args.length===0 || (
|
|
149
|
+
if(args.length===0 || (['--help','-h'].includes(args[0]) && (args.length===1 || (args.length===2 && args[1]==='--json'))))return {command:'help'};
|
|
145
150
|
if(args[0]==='launch')return parseLaunch(args);
|
|
151
|
+
if(args[0]==='compat')return parseCompat(args);
|
|
146
152
|
if(args[0]==='rebind')return parseRebind(args);
|
|
147
153
|
if(args[0]==='reset')return parseReset(args);
|
|
148
154
|
if(['setup','update','repair','remove','switch','continue'].includes(args[0]))return parseLifecycle(args);
|
|
@@ -259,7 +265,7 @@ function parseLifecycle(args) {
|
|
|
259
265
|
return result;
|
|
260
266
|
}
|
|
261
267
|
|
|
262
|
-
async function runLifecycle(command,registry,stdout,stderr) {
|
|
268
|
+
async function runLifecycle(command,registry,stdout,stderr,compatibility) {
|
|
263
269
|
// A trusted embedding/CLI assembly can supply functions; argv and package
|
|
264
270
|
// data cannot. Until real adapters ship, the default command fails before IO.
|
|
265
271
|
if(registry===null || registry===undefined)fail('cli.providers-unavailable');
|
|
@@ -275,12 +281,22 @@ async function runLifecycle(command,registry,stdout,stderr) {
|
|
|
275
281
|
if(command.bundles)input.bundles=command.bundles;
|
|
276
282
|
if(command.recoveryPath)input.recoveryPath=command.recoveryPath;
|
|
277
283
|
if(command.rebindFile)Object.assign(input,await acceptedRebind(command.workspace,command.rebindFile));
|
|
278
|
-
|
|
284
|
+
const prepared=await prepare(input,registry);
|
|
285
|
+
await stdout(JSON.stringify(command.command==='remove'?prepared:await bindCompatibility(prepared,compatibility))+'\n');return 0;
|
|
279
286
|
}
|
|
280
|
-
const
|
|
281
|
-
const
|
|
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,
|
|
282
291
|
approval:{decision:'approve',preparedDigest:prepared.digest}},registry,
|
|
283
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
|
+
}
|
|
284
300
|
await stdout(JSON.stringify(result)+'\n');
|
|
285
301
|
return (result.status==='ready' || (['remove','continue'].includes(command.command) && result.status==='not-installed')) && result.lockRelease==='released' && !result.outputError?0:1;
|
|
286
302
|
}
|
|
@@ -384,14 +400,15 @@ async function runLogs(command,stdout,stderr) {
|
|
|
384
400
|
}finally{await lock.release();}
|
|
385
401
|
}
|
|
386
402
|
|
|
387
|
-
export async function runCli(args,{stdout,stderr,registry=null}) {
|
|
403
|
+
export async function runCli(args,{stdout,stderr,registry=null,compatibility=null}) {
|
|
388
404
|
if(typeof stdout!=='function' || typeof stderr!=='function')fail('cli.transport');
|
|
389
405
|
try {
|
|
390
406
|
const command=parseCommand(args);
|
|
391
407
|
if(command.command==='help') {await stdout(help+'\n');return 0;}
|
|
408
|
+
if(command.command==='compat')return await runCompat(command,compatibility,stdout);
|
|
392
409
|
if(command.command==='launch')return await runLaunch(command,stdout,stderr);
|
|
393
410
|
if(command.command==='rebind')return await runRebind(command,stdout);
|
|
394
|
-
if(command.command==='reset')return await runReset(command,registry,stdout,stderr);
|
|
411
|
+
if(command.command==='reset')return await runReset(command,registry,stdout,stderr,compatibility);
|
|
395
412
|
if(command.command==='migration')return await runMigrationCommand(command,stdout,stderr);
|
|
396
413
|
if(command.command==='logs')return await runLogs(command,stdout,stderr);
|
|
397
414
|
if(command.command==='policy')return await runPolicy(command,stdout);
|
|
@@ -400,9 +417,9 @@ export async function runCli(args,{stdout,stderr,registry=null}) {
|
|
|
400
417
|
['abandon','continue-abandon'].includes(command.action)?runRepositoryAbandon:
|
|
401
418
|
command.action==='recover-locks'?runRepositoryLockRecovery:
|
|
402
419
|
['recover-bootstrap','continue-bootstrap','retire-bootstrap'].includes(command.action)?runBootstrapContinuation:runRepositoryRecovery)(command,stdout);
|
|
403
|
-
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);
|
|
404
421
|
const options=command.recoveryPath===undefined?{}:{recoveryPath:command.recoveryPath};
|
|
405
|
-
const result=await inspectInstallation(command.workspace,options);
|
|
422
|
+
const result=await doctorCompatibility(await inspectInstallation(command.workspace,options),compatibility);
|
|
406
423
|
await stdout(JSON.stringify(result)+'\n');
|
|
407
424
|
return result.ready?0:1;
|
|
408
425
|
} catch(error) {
|
package/src/commands/launch.js
CHANGED
|
@@ -11,8 +11,9 @@ export function parseLaunch(args) {
|
|
|
11
11
|
const result = { command: 'launch', provider: 'grok', execute: false, inspect: false }, seen = new Set();
|
|
12
12
|
for (let i = 2; i < args.length; i++) {
|
|
13
13
|
const flag = args[i];
|
|
14
|
-
if (!['--workspace', '--executable', '--execute', '--inspect'].includes(flag) || seen.has(flag)) fail('cli.arguments');
|
|
14
|
+
if (!['--workspace', '--executable', '--execute', '--inspect', '--json'].includes(flag) || seen.has(flag)) fail('cli.arguments');
|
|
15
15
|
seen.add(flag);
|
|
16
|
+
if (flag === '--json') continue;
|
|
16
17
|
if (flag === '--execute') { result.execute = true; continue; }
|
|
17
18
|
if (flag === '--inspect') { result.inspect = true; continue; }
|
|
18
19
|
const value = args[++i];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Presentation boundary only: internal command results and saved JSON contracts
|
|
2
|
+
// remain unchanged. Never infer success from the shape of a rendered result.
|
|
3
|
+
const safe = value => String(value).replace(/[\u0000-\u001f\u007f-\u009f]/g, c => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
4
|
+
const privateKey = /bytes|base64|content|secret|token|password/i;
|
|
5
|
+
|
|
6
|
+
export function formatResult(value) {
|
|
7
|
+
if (value && typeof value.ready === 'boolean' && Array.isArray(value.diagnostics)) {
|
|
8
|
+
const lines = ['Workspace Pipeline — doctor', '',
|
|
9
|
+
`Workspace: ${safe(value.workspace ?? '(not reported)')}`,
|
|
10
|
+
`Status: ${value.ready ? 'READY' : 'NOT READY'} (${safe(value.status ?? 'unknown')})`];
|
|
11
|
+
if (value.pipeline) lines.push(`Pipeline: ${safe(value.pipeline.id)} @ ${safe(value.pipeline.version)}`,
|
|
12
|
+
`Providers: ${value.pipeline.providers.map(safe).join(', ')}`);
|
|
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.');
|
|
19
|
+
lines.push('', `Diagnostics: ${value.diagnostics.length}`);
|
|
20
|
+
for (const item of value.diagnostics) lines.push(` - ${safe(item.code)}${item.subject ? ': ' + safe(item.subject) : ''}${item.pointer ? ' ' + safe(item.pointer) : ''}`);
|
|
21
|
+
lines.push('', 'Runtime / model-visible skills / MCP: not verified by doctor.',
|
|
22
|
+
'Full report: repeat with --json.');
|
|
23
|
+
return lines.join('\n') + '\n';
|
|
24
|
+
}
|
|
25
|
+
const lines = ['Workspace Pipeline', ''];
|
|
26
|
+
function render(node, indent = '', depth = 0) {
|
|
27
|
+
if (node === null || typeof node !== 'object') { lines.push(indent + safe(node)); return; }
|
|
28
|
+
if (depth > 5) { lines.push(indent + '(details omitted; use --json)'); return; }
|
|
29
|
+
const entries = Object.entries(node);
|
|
30
|
+
for (const [key, item] of entries.slice(0, 40)) {
|
|
31
|
+
const label = indent + safe(key) + ':';
|
|
32
|
+
if (privateKey.test(key)) lines.push(label + ' (private payload; use --json)');
|
|
33
|
+
else if (item !== null && typeof item === 'object') { lines.push(label); render(item, indent + ' ', depth + 1); }
|
|
34
|
+
else { const text=safe(item); lines.push(label + ' ' + (text.length>500?text.slice(0,500)+' (truncated; use --json)':text)); }
|
|
35
|
+
}
|
|
36
|
+
if (entries.length > 40) lines.push(indent + `(${entries.length - 40} more entries; use --json)`);
|
|
37
|
+
}
|
|
38
|
+
render(value);
|
|
39
|
+
lines.push('', 'For complete machine output or a saved apply preview, repeat with --json.',
|
|
40
|
+
'This display is not an apply preview. JSON may contain private configuration.');
|
|
41
|
+
return lines.join('\n') + '\n';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function outputWriter(write, json) {
|
|
45
|
+
if (json) return write;
|
|
46
|
+
return text => {
|
|
47
|
+
let value;
|
|
48
|
+
try { value = JSON.parse(text); } catch { return write(text); }
|
|
49
|
+
return write(formatResult(value));
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/commands/rebind.js
CHANGED
|
@@ -9,8 +9,9 @@ export function parseRebind(args) {
|
|
|
9
9
|
const result={command:'rebind'},seen=new Set();
|
|
10
10
|
for(let i=1;i<args.length;i++) {
|
|
11
11
|
const flag=args[i];
|
|
12
|
-
if(!['--workspace','--manifest'].includes(flag) || seen.has(flag))fail('cli.arguments');
|
|
12
|
+
if(!['--workspace','--manifest','--json'].includes(flag) || seen.has(flag))fail('cli.arguments');
|
|
13
13
|
seen.add(flag);
|
|
14
|
+
if(flag==='--json')continue;
|
|
14
15
|
const value=args[++i];if(!value || value.startsWith('--'))fail('cli.arguments');
|
|
15
16
|
result[flag==='--workspace'?'workspace':'manifestPath']=absoluteRoot(value);
|
|
16
17
|
}
|
package/src/commands/reset.js
CHANGED
|
@@ -6,13 +6,15 @@ 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();
|
|
12
13
|
for(let i=1;i<args.length;i++){
|
|
13
14
|
const flag=args[i];
|
|
14
|
-
if(!['--workspace','--to','--all','--providers','--bundles','--apply','--preview'].includes(flag) || seen.has(flag))fail('cli.arguments');
|
|
15
|
+
if(!['--workspace','--to','--all','--providers','--bundles','--apply','--preview','--json'].includes(flag) || seen.has(flag))fail('cli.arguments');
|
|
15
16
|
seen.add(flag);
|
|
17
|
+
if(flag==='--json')continue;
|
|
16
18
|
if(flag==='--apply'){result.apply=true;continue;}
|
|
17
19
|
if(flag==='--all'){(result.options??={}).all=true;continue;}
|
|
18
20
|
const value=args[++i];if(!value || value.startsWith('--'))fail('cli.arguments');
|
|
@@ -34,12 +36,13 @@ async function history(workspace){
|
|
|
34
36
|
await assertNoRepositoryPending(workspace);
|
|
35
37
|
const h=await inspectHistory(workspace);if(!h.complete || h.diagnostics.length)fail('reset.history');
|
|
36
38
|
}
|
|
37
|
-
export async function runReset(command,registry,stdout,stderr){
|
|
39
|
+
export async function runReset(command,registry,stdout,stderr,compatibility=null){
|
|
38
40
|
if(!command.apply){
|
|
39
41
|
await history(command.workspace);
|
|
40
|
-
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;
|
|
41
43
|
}
|
|
42
|
-
const
|
|
44
|
+
const unwrapped=await unwrapCompatibility((await readRecord(command.previewFile)).value,compatibility);
|
|
45
|
+
const raw=unwrapped.prepared;
|
|
43
46
|
const approval={decision:'approve',preparedDigest:raw.digest};
|
|
44
47
|
const prepared=validateResetRecord(raw,approval);
|
|
45
48
|
if(prepared.preview.plan.workspace!==command.workspace)fail('reset.workspace');
|
|
@@ -58,6 +61,7 @@ export async function runReset(command,registry,stdout,stderr){
|
|
|
58
61
|
result.status=applied.status;
|
|
59
62
|
}catch(error){result.error=error instanceof ContractError?error.code:'reset.io';}
|
|
60
63
|
finally{if(lock)try{await lock.release();result.lockRelease='released';}catch{result.lockRelease='failed';}}
|
|
61
|
-
await
|
|
62
|
-
|
|
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;
|
|
63
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()&¤t!==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
|
+
}
|