docks-kit 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -14,10 +14,10 @@ Tool-specific instructions live alongside this file:
14
14
 
15
15
  | Path | Purpose |
16
16
  |------|---------|
17
- | `docks-kit` | CLI launcher: runs the compiled binary in `cli/dist/` when present, otherwise Bun-from-source (auto-installs Bun + `node_modules`). No-Bun recovery is the platform release binary |
18
- | `cli/src/engine-native/` | EngineNative implementation for `sync`, `model`, and `toolchain`; idempotent, flag-gated for destructive reconciliation |
17
+ | `docks-kit` | CLI launcher: runs the platform binary in `cli/dist/` only when its `--version` matches `package.json`, otherwise Bun-from-source (auto-installs Bun + `node_modules`). No-Bun recovery is the standalone platform release binary |
18
+ | `cli/src/engine-native/` | EngineNative implementation for `sync`, `model`, `workflow`, and `toolchain`; idempotent, flag-gated for destructive reconciliation |
19
19
  | `cli/` | Effect-TS CLI + bundled docs topics |
20
- | `SoT/models.json` | Kit-verified model catalog |
20
+ | `SoT/models.json` | Kit-verified model catalog plus the strict Docks workflow-role registry |
21
21
  | `SoT/toolchain.json` | Toolchain floors manifest (verified pins consumed by EngineNative) |
22
22
  | `SoT/.claude/bin/` | Dependency-free Bun runtime programs for Claude's statusline, SessionStart, and Notification |
23
23
  | `install.sh` | Global installer |
@@ -36,6 +36,7 @@ Codex SoT notes:
36
36
  - `SoT/.codex/rules/*.rules` deploys to `~/.codex/rules/` as kit-managed Codex command policy. This is Codex's equivalent of permission allow/prompt/block rules; user-learned approvals in `~/.codex/rules/default.rules` are preserved.
37
37
  - `SoT/.codex/plugins/marketplace.json` deploys to Codex's personal marketplace path at `~/.agents/plugins/marketplace.json`; when the `codex` CLI is available, sync reruns `codex plugin add <plugin@marketplace>` for enabled SoT plugins so stale cached installs are refreshed.
38
38
  - `docks-kit status` verifies Session Relay only through the supported `codex plugin list --json` inventory. `ready` means installed and enabled for a newly started Codex session; it is not evidence about an old process, lifecycle state, receive-path health, or worker quiescence. The global prompt SoTs carry the owner's standing authorization for Docks cross-company plan review, which never overrides host or platform denial.
39
+ - Both global prompt SoTs carry one byte-identical compact `Docks-workflow-models:` record. Root `--model-orchestrator` / `--model-reviewer` / `--model-implementer` / review-bound flags update only that deployed record; `docks-kit models workflow` lists the closed selectors, and a flag-less sync restores defaults.
39
40
  - The `codex` CLI binary is upstream-owned, not kit-owned. The official standalone installer keeps package metadata under `$CODEX_HOME/packages/standalone` and places the `codex` symlink in `~/.local/bin` by default; sync only warns with a download-then-run installer command when the CLI is missing. Existing installs can self-update with `codex update`; npm and Homebrew remain upstream alternatives.
40
41
  - `SoT/.codex/AGENTS.md` deliberately does not import `@RTK.md`: RTK's published Codex integration is prompt-file based rather than hook based, so importing it leaks implementation detail into agent-visible context. Use Codex hooks for RTK only after the kit installs a hook-backed Codex integration.
41
42
  - Claude runtime settings are an authoring template with sentinels. `claudeRuntime.ts` materializes absolute Bun/script paths only after the shared `bun.ts` bootstrap is ready; `claudeSync.ts` writes all runtime assets before atomically committing settings, then prunes the legacy shell scripts and Stop hook. Native `rate_limits` is the sole quota source, so jq/curl/OAuth caches are not runtime dependencies. A missing Bun defers only this cutover and preserves legacy pointers/files.
@@ -47,7 +48,7 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`), see the matching SoT d
47
48
 
48
49
  - **Idempotent operations.** Every EngineNative sync step must be safe to re-run. Settings merges, plugin installs, and marketplace adds are all idempotent — re-running with no SoT changes is a no-op.
49
50
  - **Removed bash engine.** The bash engine was removed after the `bash-engine-final` tag. `DOCKS_KIT_ENGINE=bash` must fail with the removed-engine message; engine bugs are fixed forward in EngineNative.
50
- - **Targeted syncs.** `./docks-kit sync` accepts positional targets: `claude`, `codex`, and `agents`. Use the narrowest target that matches the SoT change (for example, `./docks-kit sync codex` for Codex-only config edits); targets can be combined with `--dry-run`, `--skip-rtk`, `--reconcile`, `--prune`, `--yes` (auto-accept toolchain prompts), and the deploy-time modifiers `--claude-compact-window=<tokens>` / `--claude-permissive` / `--claude-model=<m>` / `--claude-effort=<level>` / `--claude-advisor=<on|off|default>` / `--codex-model=<m>` / `--codex-effort=<level>` (see `CLAUDE.md` § Deploy-time modifiers).
51
+ - **Targeted syncs.** `./docks-kit sync` accepts positional targets: `claude`, `codex`, and `agents`. Use the narrowest target that matches the SoT change (for example, `./docks-kit sync codex` for Codex-only config edits); targets can be combined with `--dry-run`, `--skip-rtk`, `--skip-plugin-refresh` (install missing plugins without refreshing existing caches; used by `docks-kit update`), `--reconcile`, `--prune`, `--yes` (auto-accept toolchain prompts), and the deploy-time modifiers `--claude-compact-window=<tokens>` / `--claude-permissive` / `--claude-model=<m>` / `--claude-effort=<level>` / `--claude-advisor=<on|off|default>` / `--codex-model=<m>` / `--codex-effort=<level>` (see `CLAUDE.md` § Deploy-time modifiers).
51
52
  - **Additive by default.** Keys present in deployed config but absent from SoT are preserved on default sync. This protects user-only additions, but means drift accumulates — neither flag-less reset can clean it up. The one exception is the Claude `removed` manifest (`claude::_removed_manifest`), a curated list of unambiguous kit-owned artifacts that `claude::sync_removals` force-prunes on every sync; see `CLAUDE.md` § Pruning stale artifacts.
52
53
  - **`--reconcile` / `--prune` are the kit-owned reconcile flags.** Orthogonal — `--reconcile` reconciles the settings layer (SoT-declared keys/tables/arrays win; user-only keys and nested objects are preserved; permissions arrays are replaced wholesale by SoT). `--prune` uninstalls kit-managed installations not in the SoT (plugins, marketplaces, and `~/.agents/skills/*` entries tracked in `~/.agents/.kit-managed-skills`). Combine for a full reset to SoT's kit-managed scope. User-only additions outside the kit's scope (custom env vars, mcpServers, manually-installed skills, third-party plugins not declared in SoT) are always preserved. Each tool's per-tool file documents the specific paths and diff recipes.
53
54
  - **SOLID-aligned modules.** `cli/src/engine-native/parseArgs.ts` owns flag parsing/validation. `toolchain.ts` owns the verified-version gate over `SoT/toolchain.json`; `bun.ts` owns the shared, memoized Bun bootstrap; `claudeRuntime.ts` owns Claude settings materialization. `claudeSync.ts`, `codexSync.ts`, and `skillsSync.ts` own tool-specific sync logic. `index.ts` is the thin orchestrator. The public CLI seam is `cli/src/engine.ts`.
@@ -86,10 +87,10 @@ When a kit-mechanic skill, its `references/`, or a wrapper agent (`.claude/agent
86
87
  ## Plans
87
88
 
88
89
  <constraint>
89
- Multi-commit work plans live in `docs/plans/active/` (status is a frontmatter field) and `docs/plans/finished/` (archive). Every plan file is a complete handoff document — `goal`, `Steps`, `Acceptance criteria`, `Review` — so any agent can pick one up cold. Skills handle every operation: `plan-init` (bootstrap/migrate), `plan-manager` (list/show/start/block/ship/new, auto-commit on transition, self-review on draft), `plan-review` (verification). Trigger by natural language or the matching `plan-*` skill. `active/` is multi-occupancy.
90
+ Multi-commit work plans live in `docs/plans/active/` (status is a frontmatter field) and `docs/plans/finished/` (archive). Every plan file is a complete cold-handoff document — goal, context & rationale, environment & how-to-run, steps with exact paths, executable acceptance criteria, and a binary cold-handoff checklist — so any agent (or a weaker model) can pick one up cold without guessing. Skills handle every operation: `plan-init` (bootstrap/migrate), `plan-manager` (list/show/start/block/ship/new, auto-commit on transition, self-review on draft), `plan-review` (verification). Trigger by natural language or the matching `plan-*` skill. `active/` is multi-occupancy.
90
91
  </constraint>
91
92
 
92
- The full convention (frontmatter schema, body sections, self-review loop, open-questions, age tokens) lives in `docs/plans/AGENTS.md`. `docs/plans/CLAUDE.md` is a one-line `@AGENTS.md` import for Claude Code's nested discovery.
93
+ The full convention (frontmatter schema, body sections, one-pass local self-review, bounded strong-default X/S review receipts, workflow roles, open-questions, age tokens) lives in `docs/plans/AGENTS.md`. `docs/plans/CLAUDE.md` is a one-line `@AGENTS.md` import for Claude Code's nested discovery. If `.codex/agents/plan-manager.toml` and `.codex/agents/plan-review.toml` exist, Codex may use them for explicit subagent delegation; otherwise run the matching `plan-*` skill inline.
93
94
 
94
95
  Distinct from per-tool **Open Concerns** sections (wait-on-upstream
95
96
  blockers tied to a vendor shipping a fix — these live inside the per-tool
package/README.md CHANGED
@@ -17,8 +17,10 @@ cd ~/projects/public
17
17
  ./docks-kit status # doctor view: drift, toolchain, plugins, skills
18
18
  ```
19
19
 
20
- The `./docks-kit` launcher prefers a compiled binary (`cli/dist/`), then runs
21
- from source via Bun — auto-installing Bun and dependencies when missing.
20
+ The `./docks-kit` launcher prefers a compiled binary (`cli/dist/`) only when
21
+ its version matches the checkout, then runs from source via Bun — auto-installing
22
+ Bun and dependencies when missing. Stale ignored build artifacts cannot mask
23
+ newer checkout code.
22
24
 
23
25
  Other install paths (global `bun add -g docks-kit`, curl installer) —
24
26
  see `./docks-kit docs install`.
@@ -39,7 +41,7 @@ RTK/Bun download boundaries, not as a global sync prerequisite.
39
41
  docks-kit sync [claude] [codex] [agents] deploy the SoT (default: all three)
40
42
  docks-kit update [--no-sync] self-update the kit (autodetects checkout vs global install), then sync
41
43
  docks-kit model <claude|codex> [value] get/set the DEPLOYED model (TTY picker)
42
- docks-kit models [tool] [--json] kit-verified model catalog
44
+ docks-kit models [claude|codex|workflow] model and workflow-role catalogs (`--json`)
43
45
  docks-kit toolchain [check|ensure <tool>] verified-version floors for external tools
44
46
  docks-kit status [--json] deployed-vs-SoT drift + toolchain + counts
45
47
  docks-kit plugins list [--json] enabledPlugins tri-state vs installed
@@ -65,12 +67,28 @@ golden-regression coverage for dry-run output, mutation snapshots, and argv logs
65
67
  | `--claude-plugin=<name>` | Sticky opt-in plugin (supabase, n8n) |
66
68
  | `--codex-model=<m>` | Deploy-time modifier: deployed Codex model |
67
69
  | `--skip-rtk` | Skip optional tool bootstrap |
70
+ | `--skip-plugin-refresh` | Install missing plugins but skip refresh-only updates (used automatically by `docks-kit update`) |
68
71
  | `--yes` | Auto-accept toolchain prompts (CI/containers) |
69
72
 
70
73
  **Deploy-time modifiers** change deployed config only — the SoT is untouched
71
74
  and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
72
75
  (includes the old→new rename table for the pre-CLI `sync.sh` flags).
73
76
 
77
+ ### Docks workflow roles (root flags)
78
+
79
+ | Flag | Effect |
80
+ |------|--------|
81
+ | `--model-orchestrator=<selector>` | Override the Docks orchestrator candidate/profile |
82
+ | `--model-reviewer=<selector>` | Override the Docks reviewer candidate/profile |
83
+ | `--model-implementer=<selector>` | Override the Docks implementer candidate/profile |
84
+ | `--review-min-score=<0..100>` | Override the completion-review target |
85
+ | `--review-max-rounds=<1..10>` | Bound completion-review batches |
86
+
87
+ Selectors are strict `profile:<name>` or `<tool>:<model>@<effort>` values from
88
+ `docks-kit models workflow`. These root flags update only the identical compact
89
+ record in `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`; omitted fields retain
90
+ their current valid values, while a later flag-less sync restores all defaults.
91
+
74
92
  ## How syncing works
75
93
 
76
94
  - **Additive by default** — user-only settings keys, plugins, and skills
@@ -83,7 +101,7 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
83
101
  (`--yes` accepts; non-TTY falls back to the pinned verified version
84
102
  when possible). `docks-kit toolchain check` shows the full table.
85
103
  - **Model catalog** — `SoT/models.json` is the research-verified source for
86
- model validation, listings, and pickers.
104
+ model validation, listings, pickers, and the Docks workflow-role registry.
87
105
  - **Claude runtime** — sync materializes three dependency-free Bun `.mjs`
88
106
  programs for statusline, SessionStart, and Notification. Quota display uses
89
107
  Claude's native `rate_limits`; there is no OAuth fetch, shared usage cache,
@@ -97,7 +115,7 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
97
115
  | `SoT/.claude/` | Claude Code SoT (settings template, Bun runtime programs, CLAUDE.md) |
98
116
  | `SoT/.codex/` | Codex SoT (config.toml, rules, AGENTS.md, marketplace) |
99
117
  | `SoT/.agents/` | Universal-skill manifest |
100
- | `SoT/models.json` | Kit-verified model catalog |
118
+ | `SoT/models.json` | Kit-verified model and Docks workflow-role catalog |
101
119
  | `SoT/toolchain.json` | Verified-version floors |
102
120
  | `cli/src/engine-native/` | EngineNative sync/model/toolchain implementation |
103
121
  | `cli/src/generated/sotPayload.ts` | Generated in-memory payload used by standalone and npm installs |
package/cli/docs/flags.md CHANGED
@@ -19,6 +19,7 @@ docks-kit sync claude agents # two
19
19
  | `--reconcile` | Settings layer reconciled toward SoT (SoT keys win; user-only keys preserved; permissions arrays replaced) |
20
20
  | `--prune` | Uninstall kit-managed installs not in SoT: plugins, marketplaces, universal skills |
21
21
  | `--skip-rtk` | Skip optional tool bootstrap (RTK, bubblewrap) |
22
+ | `--skip-plugin-refresh` | Install missing Claude/Codex plugins but skip refresh-only updates; `docks-kit update` uses this automatically |
22
23
  | `--yes` | Auto-accept toolchain above-verified prompts (containers/CI) |
23
24
  | `--verbose` / `-v` | Also print no-op confirmations (already in sync, up to date, left as-is); accepted on `sync`, `model`, and `toolchain` |
24
25
 
@@ -39,6 +40,23 @@ Bare model, effort, or advisor modifiers print the relevant valid-value catalog
39
40
  and exit 2. A modifier for a target not selected by the positional arguments is
40
41
  ignored with a warning; Claude modifiers never touch Codex config and vice versa.
41
42
 
43
+ ## Docks workflow root flags
44
+
45
+ These flags belong at the root (`docks-kit --model-reviewer=...`), not after
46
+ `sync`. They update only the shared deployed workflow record.
47
+
48
+ | Flag | Accepted value |
49
+ |------|----------------|
50
+ | `--model-orchestrator=<selector>` | `profile:<name>` or `<tool>:<model>@<effort>` |
51
+ | `--model-reviewer=<selector>` | same strict selector grammar |
52
+ | `--model-implementer=<selector>` | same strict selector grammar |
53
+ | `--review-min-score=<n>` | base-10 integer `0..100` |
54
+ | `--review-max-rounds=<n>` | base-10 integer `1..10` |
55
+
56
+ Use `docks-kit models workflow [--json]` for the closed registry. Bare, empty,
57
+ unknown, or malformed values print that helper and exit 2 before either prompt
58
+ file changes.
59
+
42
60
  ## Renamed legacy flags (pre-CLI sync.sh)
43
61
 
44
62
  Old flags exit with a rename hint — there is no compat behavior.
@@ -8,9 +8,10 @@ cd ~/projects/public
8
8
  ./docks-kit sync
9
9
  ```
10
10
 
11
- The `./docks-kit` launcher prefers a compiled binary in `cli/dist/`, then
12
- falls back to Bun-from-source (auto-installing Bun via download-then-run and
13
- `bun install --frozen-lockfile` when needed).
11
+ The `./docks-kit` launcher prefers a compiled binary in `cli/dist/` only when
12
+ its reported version matches `package.json`, then falls back to Bun-from-source
13
+ (auto-installing Bun via download-then-run and `bun install --frozen-lockfile`
14
+ when needed). An ignored stale build is reported and bypassed.
14
15
 
15
16
  ## 2. Global via Bun (effect-solutions-style)
16
17
 
@@ -78,7 +79,7 @@ Two supported paths (CI-verified on windows-2025, native PowerShell):
78
79
  ## Keeping the kit up to date
79
80
 
80
81
  ```
81
- docks-kit update # autodetect + update + chained flag-less sync
82
+ docks-kit update # autodetect + update + install-missing-only sync
82
83
  docks-kit update --no-sync # update only
83
84
  ```
84
85
 
@@ -86,8 +87,10 @@ Autodetection: a kit home with `.git` is a checkout (requires a clean
86
87
  worktree and an upstream; `git pull --ff-only`, re-runs
87
88
  `bun install --frozen-lockfile` when the lockfile changed); a kit home
88
89
  under `node_modules` is a global package (`bun add -g` /
89
- `npm install -g docks-kit@latest`). A compiled binary inside a checkout
90
- updates the checkout and tells you to rebuild/download the binary.
90
+ `npm install -g docks-kit@latest`). The chained sync skips refresh-only work
91
+ for already-installed Claude/Codex plugins but still installs missing ones.
92
+ A compiled binary inside a checkout updates the checkout; on the next invocation
93
+ the launcher bypasses that now-stale binary and uses updated source until rebuilt.
91
94
  Every `docks-kit sync` also does a best-effort behind-upstream check and
92
95
  nudges when the checkout is stale (silent offline / detached / no git).
93
96
 
@@ -31,8 +31,27 @@ docks-kit models claude --json # machine-readable
31
31
  docks-kit model claude # current deployed + SoT + picker (TTY)
32
32
  docks-kit model claude opus # per-machine override from the Fable SoT
33
33
  docks-kit sync claude --claude-model=opus # same, as part of a sync
34
+ docks-kit models workflow # Docks workflow profiles and defaults
35
+ docks-kit models workflow --json # closed machine-readable registry
34
36
  ```
35
37
 
38
+ ## Docks workflow-role registry
39
+
40
+ The `workflow` section is strict even though ordinary tool-model modifiers are
41
+ forward-compatible. It accepts only known `profile:<name>` values or exact
42
+ `<tool>:<model>@<effort>` targets whose model and effort both appear in the
43
+ catalog. The defaults are:
44
+
45
+ - orchestrator: `profile:claude-best`, ordered as `claude:fable@high` then
46
+ `claude:opus@xhigh`;
47
+ - reviewer and implementer: `codex:gpt-5.6-sol@xhigh`;
48
+ - review minimum score 90 and maximum 3 rounds.
49
+
50
+ `claude:best@high` is Claude's native one-model alias. It is deliberately not
51
+ the same as the Docks-managed `profile:claude-best` candidate chain.
52
+ Availability is `checked_when_used`: Docks classifies each launch attempt;
53
+ docks-kit neither probes providers nor promises provider-wide fallback.
54
+
36
55
  ## Advisor pairing note (Claude)
37
56
 
38
57
  The SoT ships `model: fable` with advisor off (`advisorModel` unset).
@@ -44,6 +44,29 @@ docks-kit model codex gpt-5.5 # codex flavor
44
44
  The set path calls the same engine functions as the sync flags —
45
45
  one implementation, two entry points.
46
46
 
47
+ ## Workflow-role overrides (root, no full sync)
48
+
49
+ The root workflow flags are deployed modifiers for Docks rather than one tool:
50
+
51
+ ```text
52
+ docks-kit --model-orchestrator=profile:claude-best \
53
+ --model-reviewer=codex:gpt-5.6-terra@high \
54
+ --review-min-score=80 --review-max-rounds=5
55
+ ```
56
+
57
+ They atomically write one byte-identical complete record to
58
+ `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`. Omitted fields retain the
59
+ current valid record; a missing side is repaired. Invalid or conflicting state
60
+ stops before mutation, a repeated request is a no-op, and failure on the second
61
+ atomic write restores both snapshots. A later flag-less sync restores the
62
+ embedded defaults. Start fresh sessions after changing the record.
63
+
64
+ Selectors are strict `profile:<name>` or `<tool>:<model>@<effort>` entries from
65
+ `docks-kit models workflow`. `profile:claude-best` is the ordered Docks
66
+ Fable-high then Opus-xhigh chain; `claude:best@high` is instead Claude's native
67
+ single-model alias. Docks checks each candidate when used—there is no docks-kit
68
+ provider preflight.
69
+
47
70
  ## Sticky opt-ins are NOT modifiers
48
71
 
49
72
  `--claude-plugin=<name>` installs+enables an optional plugin and it STAYS
@@ -17,7 +17,7 @@ AI-assisted dev environment on every machine.
17
17
  | `cli/src/generated/sotPayload.ts` | Deterministic generated payload embedded in standalone/npm execution |
18
18
  | `cli/src/engine-native/` | EngineNative mutation logic for sync/model/toolchain |
19
19
  | `cli/` | This CLI (Effect-TS on Bun) plus bundled docs |
20
- | `docks-kit` | Launcher: compiled binary → bun-from-source, with Bun auto-install |
20
+ | `docks-kit` | Launcher: version-matching compiled binary → bun-from-source, with Bun auto-install |
21
21
 
22
22
  ## Design rules
23
23
 
@@ -2,9 +2,10 @@ import { Args, Command, Options } from "@effect/cli"
2
2
  import { Console, Effect, Option } from "effect"
3
3
  import { bail } from "../engine"
4
4
  import { modelCatalog, type Tool } from "../manifests"
5
+ import { workflowCatalog, workflowRegistryJson } from "../workflowModels"
5
6
 
6
7
  const tool = Args.text({ name: "tool" }).pipe(
7
- Args.withDescription("claude | codex (omit for both)"),
8
+ Args.withDescription("claude | codex | workflow (omit for both tool catalogs)"),
8
9
  Args.optional
9
10
  )
10
11
  const json = Options.boolean("json").pipe(
@@ -24,8 +25,11 @@ const renderTool = (t: Tool) =>
24
25
  export const modelsCommand = Command.make("models", { tool, json }, (config) =>
25
26
  Effect.gen(function* () {
26
27
  const requested = Option.getOrUndefined(config.tool)
28
+ if (requested === "workflow") {
29
+ return yield* Console.log(config.json ? workflowRegistryJson() : workflowCatalog())
30
+ }
27
31
  if (requested !== undefined && requested !== "claude" && requested !== "codex") {
28
- return yield* bail(`Unknown tool '${requested}' (valid: claude, codex)`)
32
+ return yield* bail(`Unknown tool '${requested}' (valid: claude, codex, workflow)`)
29
33
  }
30
34
  const tools: Array<Tool> = requested !== undefined ? [requested as Tool] : ["claude", "codex"]
31
35
 
@@ -42,5 +46,5 @@ export const modelsCommand = Command.make("models", { tool, json }, (config) =>
42
46
  )
43
47
  })
44
48
  ).pipe(
45
- Command.withDescription("List the kit-verified model catalog (SoT/models.json) for claude/codex.")
49
+ Command.withDescription("List kit-verified tool models or the Docks workflow role registry (SoT/models.json).")
46
50
  )
@@ -80,6 +80,9 @@ const prune = Options.boolean("prune").pipe(
80
80
  const skipRtk = Options.boolean("skip-rtk").pipe(
81
81
  Options.withDescription("Skip optional tool bootstrap (RTK, bubblewrap)")
82
82
  )
83
+ const skipPluginRefresh = Options.boolean("skip-plugin-refresh").pipe(
84
+ Options.withDescription("Install missing plugins but skip refresh-only updates for existing plugins")
85
+ )
83
86
  const yes = Options.boolean("yes").pipe(
84
87
  Options.withDescription("Auto-accept toolchain prompts (containers/CI)")
85
88
  )
@@ -129,6 +132,7 @@ export const syncCommand = Command.make(
129
132
  reconcile,
130
133
  prune,
131
134
  skipRtk,
135
+ skipPluginRefresh,
132
136
  yes,
133
137
  verbose,
134
138
  claudeModel,
@@ -172,6 +176,7 @@ export const syncCommand = Command.make(
172
176
  if (config.reconcile) args.push("--reconcile")
173
177
  if (config.prune) args.push("--prune")
174
178
  if (config.skipRtk) args.push("--skip-rtk")
179
+ if (config.skipPluginRefresh) args.push("--skip-plugin-refresh")
175
180
  if (config.yes) args.push("--yes")
176
181
  if (config.verbose) args.push("--verbose")
177
182
  if (config.claudePermissive) args.push("--claude-permissive")
@@ -26,6 +26,12 @@ const chainSync = (argv0: string, args: Array<string>): Effect.Effect<void> =>
26
26
  if (res.error !== undefined || res.status !== 0) process.exit(res.status ?? 1)
27
27
  })
28
28
 
29
+ export const updateSyncArgs = (home: string): Array<string> => [
30
+ join(home, "cli/src/main.ts"),
31
+ "sync",
32
+ "--skip-plugin-refresh"
33
+ ]
34
+
29
35
  const updateCheckout = (home: string, skipSync: boolean) =>
30
36
  Effect.gen(function* () {
31
37
  if (spawnSync("git", ["--version"], { stdio: "ignore" }).status !== 0) {
@@ -62,12 +68,12 @@ const updateCheckout = (home: string, skipSync: boolean) =>
62
68
 
63
69
  if (compiled) {
64
70
  return yield* Console.log(
65
- "This compiled binary still runs the previous version - rebuild (bash cli/build-binaries.sh) or download the latest release binary, then run: docks-kit sync"
71
+ "This compiled binary still runs the previous version - the checkout launcher will use updated source next time. Run: ./docks-kit sync (rebuild with bash cli/build-binaries.sh to restore the binary fast path)."
66
72
  )
67
73
  }
68
74
  if (skipSync) return yield* Console.log("Kit updated. Run: docks-kit sync")
69
75
  yield* Console.log("Kit updated - running sync with the new version...")
70
- return yield* chainSync(process.execPath, [join(home, "cli/src/main.ts"), "sync"])
76
+ return yield* chainSync(process.execPath, updateSyncArgs(home))
71
77
  })
72
78
 
73
79
  const updatePackage = (home: string, skipSync: boolean) =>
@@ -93,7 +99,7 @@ const updatePackage = (home: string, skipSync: boolean) =>
93
99
  yield* Console.log("Kit updated - running sync with the new version...")
94
100
  // Chain through the package dir just updated (global installs update in
95
101
  // place) — a bare `docks-kit` PATH lookup could hit a different shim.
96
- return yield* chainSync(process.execPath, [join(home, "cli/src/main.ts"), "sync"])
102
+ return yield* chainSync(process.execPath, updateSyncArgs(home))
97
103
  })
98
104
 
99
105
  export const updateCommand = Command.make("update", { noSync }, (config) =>
@@ -111,6 +117,6 @@ export const updateCommand = Command.make("update", { noSync }, (config) =>
111
117
  })
112
118
  ).pipe(
113
119
  Command.withDescription(
114
- "Self-update the kit: autodetects the install (git checkout -> ff-only pull; bun/npm global -> @latest) and chains a flag-less sync with the new version (--no-sync to skip)."
120
+ "Self-update the kit: autodetects the install (git checkout -> ff-only pull; bun/npm global -> @latest) and chains an install-missing-only sync with the new version (--no-sync to skip)."
115
121
  )
116
122
  )
@@ -31,6 +31,7 @@ import { ExitError } from "./parseArgs"
31
31
  import { mergeSettings, reconcileSettings } from "./settings"
32
32
  import { ensure, field } from "./toolchain"
33
33
  import { payloadBytes, payloadDisplayPath, payloadText } from "../payload"
34
+ import { renderDefaultWorkflowInstructions } from "./workflowDeploy"
34
35
 
35
36
  export type ClaudeRuntimeState =
36
37
  | { readonly kind: "ready"; readonly paths: ClaudeRuntimePaths }
@@ -222,7 +223,7 @@ function syncClaudeMd(ctx: Ctx, claudeDir: string): void {
222
223
  return
223
224
  }
224
225
 
225
- const source = payloadText("SoT/.claude/CLAUDE.md")
226
+ const source = renderDefaultWorkflowInstructions(payloadText("SoT/.claude/CLAUDE.md"))
226
227
  const stripReason = ctx.skipRtk ? "--skip-rtk" : rtkMdAbsent ? "~/.claude/RTK.md absent (rtk not initialized)" : ""
227
228
  if (stripReason !== "") {
228
229
  const stripped = source
@@ -503,6 +504,10 @@ const REMOVED_MANIFEST = {
503
504
  "env.CLAUDE_CODE_FORK_SUBAGENT",
504
505
  "env.CLAUDE_CODE_EFFORT_LEVEL"
505
506
  ],
507
+ permissionRules: {
508
+ allow: ["Write(./)"],
509
+ deny: ["Write(**/.env)", "Write(**/.env.local)", "Write(**/secrets/**)"]
510
+ },
506
511
  claudeJsonKeys: [] as Array<string>,
507
512
  runtimeReady: {
508
513
  hooks: ["notify.sh"],
@@ -544,6 +549,30 @@ function pruneJsonKeys(ctx: Ctx, file: string, keys: Array<string>): number {
544
549
  return presentKeys.length
545
550
  }
546
551
 
552
+ function prunePermissionRules(
553
+ ctx: Ctx,
554
+ file: string,
555
+ rules: Readonly<Record<"allow" | "deny", ReadonlyArray<string>>>
556
+ ): number {
557
+ if (!existsSync(file)) return 0
558
+ const doc = parseJson(readFileSync(file, "utf8"))
559
+ if (doc === undefined || !isObject(doc) || !isObject(doc["permissions"])) return 0
560
+ const permissions = doc["permissions"]
561
+ let present = 0
562
+ for (const key of ["allow", "deny"] as const) {
563
+ const values = permissions[key]
564
+ if (!Array.isArray(values)) continue
565
+ const removed = new Set(rules[key])
566
+ present += rules[key].filter((rule) => values.includes(rule)).length
567
+ if (!ctx.dryRun) permissions[key] = values.filter((value) => typeof value !== "string" || !removed.has(value))
568
+ }
569
+ if (present > 0 && !ctx.dryRun) {
570
+ writeFileSync(`${file}.tmp`, jqStringify(doc))
571
+ renameSync(`${file}.tmp`, file)
572
+ }
573
+ return present
574
+ }
575
+
547
576
  function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState): void {
548
577
  const { change, echo } = ctx.services.logger
549
578
  let hooksRemoved = 0
@@ -591,16 +620,25 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
591
620
  }
592
621
 
593
622
  const skeys = pruneJsonKeys(ctx, p(claudeDir, "settings.json"), settingsKeys)
623
+ const permissionRules = prunePermissionRules(
624
+ ctx,
625
+ p(claudeDir, "settings.json"),
626
+ REMOVED_MANIFEST.permissionRules
627
+ )
594
628
  const cjkeys = pruneJsonKeys(ctx, p(ctx.home, ".claude.json"), REMOVED_MANIFEST.claudeJsonKeys)
595
629
 
596
630
  if (ctx.dryRun) {
597
631
  if (skeys > 0) echo(`[dry-run] del ${skeys} stale key(s) from ${p(claudeDir, "settings.json")}`)
632
+ if (permissionRules > 0) {
633
+ echo(`[dry-run] del ${permissionRules} stale permission rule(s) from ${p(claudeDir, "settings.json")}`)
634
+ }
598
635
  if (cjkeys > 0) echo(`[dry-run] del ${cjkeys} stale key(s) from ${p(ctx.home, ".claude.json")}`)
599
636
  return
600
637
  }
601
638
 
602
- if (hooksRemoved + filesRemoved + skeys + cjkeys > 0) {
603
- change(`Pruned stale artifacts (hooks: ${hooksRemoved}, files: ${filesRemoved}, settings keys: ${skeys}, claude.json keys: ${cjkeys})`)
639
+ if (hooksRemoved + filesRemoved + skeys + permissionRules + cjkeys > 0) {
640
+ const permissionSummary = permissionRules > 0 ? `, permission rules: ${permissionRules}` : ""
641
+ change(`Pruned stale artifacts (hooks: ${hooksRemoved}, files: ${filesRemoved}, settings keys: ${skeys}, claude.json keys: ${cjkeys}${permissionSummary})`)
604
642
  ctx.nextStepTriggers.claudeRestart = true
605
643
  }
606
644
  }
@@ -636,7 +674,11 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
636
674
  const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
637
675
 
638
676
  if (ctx.dryRun) {
639
- echo("[dry-run] bootstrap + update plugin marketplaces + plugins from SoT")
677
+ echo(
678
+ ctx.skipPluginRefresh
679
+ ? "[dry-run] bootstrap + install missing plugins from SoT; skip refresh-only plugin updates"
680
+ : "[dry-run] bootstrap + update plugin marketplaces + plugins from SoT"
681
+ )
640
682
  if (ctx.prune) {
641
683
  echo("[dry-run] (--prune) would also uninstall plugins not in SoT and remove extra marketplaces")
642
684
  }
@@ -694,13 +736,16 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
694
736
  }
695
737
  }
696
738
 
697
- // Pass 3 — refresh every installed plugin.
698
- cli(["plugin", "marketplace", "update"])
699
739
  let updatedPl = 0
700
740
  const installedDoc = readJsonFile(installedPlugins)
701
741
  const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
702
- for (const pluginId of installedKeys) {
703
- if (cli(["plugin", "update", pluginId]).out.includes("Successfully updated")) updatedPl++
742
+ // Pass 3 refresh every installed plugin unless the update command
743
+ // selected its install-missing-only fast path.
744
+ if (!ctx.skipPluginRefresh) {
745
+ cli(["plugin", "marketplace", "update"])
746
+ for (const pluginId of installedKeys) {
747
+ if (cli(["plugin", "update", pluginId]).out.includes("Successfully updated")) updatedPl++
748
+ }
704
749
  }
705
750
 
706
751
  // Passes 4 + 5 — prune-gated uninstall + marketplace removal.
@@ -12,6 +12,7 @@ import type { Ctx } from "./index"
12
12
  import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
13
13
  import { sessionRelayReadiness } from "./sessionRelayReadiness"
14
14
  import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
15
+ import { renderDefaultWorkflowInstructions } from "./workflowDeploy"
15
16
 
16
17
  export function codexSync(ctx: Ctx): void {
17
18
  const codexDir = p(ctx.home, ".codex")
@@ -24,7 +25,7 @@ export function codexSync(ctx: Ctx): void {
24
25
  syncCodexModel(ctx, ctx.codexModel)
25
26
  syncCodexEffort(ctx, ctx.codexEffort)
26
27
  syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
27
- syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
28
+ syncAgentsMd(ctx, renderDefaultWorkflowInstructions(payloadText("SoT/.codex/AGENTS.md")), p(codexDir, "AGENTS.md"))
28
29
  syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
29
30
  removeLegacyDocksMarketplace(ctx, userConfig)
30
31
  syncPlugins(ctx, sotConfig)
@@ -431,10 +432,30 @@ function manualPluginRefreshCommand(sotConfigText: string): string {
431
432
  return first !== undefined ? `codex plugin add ${first}` : "codex plugin add <plugin@marketplace>"
432
433
  }
433
434
 
435
+ function installedPluginIdsFromCli(): Set<string> | undefined {
436
+ const result = spawnSync("codex", ["plugin", "list", "--json"], {
437
+ encoding: "utf8",
438
+ stdio: ["ignore", "pipe", "ignore"]
439
+ })
440
+ if (result.error !== undefined || result.status !== 0) return undefined
441
+ const value = parseJson(result.stdout ?? "")
442
+ if (value === undefined || !isObject(value) || !Array.isArray(value["installed"])) return undefined
443
+ const ids = new Set<string>()
444
+ for (const row of value["installed"]) {
445
+ if (!isObject(row) || row["installed"] !== true || typeof row["pluginId"] !== "string") continue
446
+ ids.add(row["pluginId"])
447
+ }
448
+ return ids
449
+ }
450
+
434
451
  function syncPlugins(ctx: Ctx, sotConfigText: string): void {
435
- const { change, echo, warn } = ctx.services.logger
452
+ const { change, echo, verbose, warn } = ctx.services.logger
436
453
  if (ctx.dryRun) {
437
- echo("[dry-run] add enabled Codex plugins from SoT")
454
+ echo(
455
+ ctx.skipPluginRefresh
456
+ ? "[dry-run] add missing enabled Codex plugins from SoT; skip refresh-only plugin updates"
457
+ : "[dry-run] add enabled Codex plugins from SoT"
458
+ )
438
459
  return
439
460
  }
440
461
 
@@ -453,9 +474,20 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
453
474
  return
454
475
  }
455
476
 
477
+ const desiredPluginIds = enabledPluginIdsFromText(sotConfigText)
478
+ let pluginIds = desiredPluginIds
479
+ if (ctx.skipPluginRefresh) {
480
+ const installedPluginIds = installedPluginIdsFromCli()
481
+ if (installedPluginIds === undefined) {
482
+ warn("Codex plugin inventory unavailable — falling back to the full refresh path")
483
+ } else {
484
+ pluginIds = desiredPluginIds.filter((pluginId) => !installedPluginIds.has(pluginId))
485
+ }
486
+ }
487
+
456
488
  let refreshed = 0
457
489
  let failed = 0
458
- for (const pluginId of enabledPluginIdsFromText(sotConfigText)) {
490
+ for (const pluginId of pluginIds) {
459
491
  const res = spawnSync("codex", ["plugin", "add", pluginId], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
460
492
  const addOut = `${res.stdout ?? ""}${res.stderr ?? ""}`
461
493
  if (res.error === undefined && res.status === 0) {
@@ -482,6 +514,7 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
482
514
  warn(`Session Relay readiness unavailable after refresh: ${readiness.reason}`)
483
515
  }
484
516
  }
517
+ if (ctx.skipPluginRefresh && pluginIds.length === 0) verbose("Codex plugins already installed; refresh-only updates skipped")
485
518
  if (failed > 0) warn(`${failed} Codex plugin operation(s) failed — re-run sync or install manually`)
486
519
  }
487
520
 
@@ -15,7 +15,8 @@ import { claudeNextSteps, claudeSummary, claudeSync } from "./claudeSync"
15
15
  import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
16
16
  import { skillsNextSteps, skillsSummary, skillsSync } from "./skillsSync"
17
17
  import { modeModel, modeToolchain } from "./modes"
18
- import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
18
+ import { ExitError, parseArgs, parseWorkflowArgs, printWorkflowUsage, validateModifierFlags } from "./parseArgs"
19
+ import { deployWorkflowOverrides } from "./workflowDeploy"
19
20
 
20
21
  export type ModifierFlag =
21
22
  | "--claude-model"
@@ -31,6 +32,7 @@ export interface Ctx {
31
32
  dryRun: boolean
32
33
  verbose: boolean
33
34
  skipRtk: boolean
35
+ skipPluginRefresh?: boolean
34
36
  reconcile: boolean
35
37
  prune: boolean
36
38
  assumeYes: boolean
@@ -71,6 +73,7 @@ function makeCtx(services: EngineServices): Ctx {
71
73
  dryRun: env["DRY_RUN"] === "1",
72
74
  verbose: env["DOCKS_KIT_VERBOSE"] === "1",
73
75
  skipRtk: env["SKIP_RTK"] === "1",
76
+ skipPluginRefresh: false,
74
77
  reconcile: env["RECONCILE"] === "1",
75
78
  prune: env["PRUNE"] === "1",
76
79
  assumeYes: env["ASSUME_YES"] === "1",
@@ -124,6 +127,19 @@ function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
124
127
  return 0
125
128
  }
126
129
 
130
+ function engineWorkflow(ctx: Ctx, args: ReadonlyArray<string>): number {
131
+ try {
132
+ const overrides = parseWorkflowArgs(ctx, args)
133
+ deployWorkflowOverrides(ctx, overrides)
134
+ return 0
135
+ } catch (error) {
136
+ if (error instanceof ExitError) throw error
137
+ printWorkflowUsage(ctx)
138
+ ctx.services.logger.err(error instanceof Error ? error.message : String(error))
139
+ throw new ExitError(2)
140
+ }
141
+ }
142
+
127
143
  export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineServices): number {
128
144
  let ctx!: Ctx
129
145
  const baseServices = services ?? makeEngineServices()
@@ -149,6 +165,8 @@ export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineSe
149
165
  return modeModel(ctx, argv.slice(1))
150
166
  case "toolchain":
151
167
  return modeToolchain(ctx, argv.slice(1))
168
+ case "workflow":
169
+ return engineWorkflow(ctx, argv.slice(1))
152
170
  case "sync":
153
171
  return engineSync(ctx, argv.slice(1))
154
172
  default: