docks-kit 0.5.0 → 0.7.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/AGENTS.md CHANGED
@@ -15,9 +15,9 @@ Tool-specific instructions live alongside this file:
15
15
  | Path | Purpose |
16
16
  |------|---------|
17
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 |
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 |
@@ -32,9 +32,11 @@ Tool-specific instructions live alongside this file:
32
32
 
33
33
  Codex SoT notes:
34
34
  - `SoT/.codex/AGENTS.md` deploys to `~/.codex/AGENTS.md` as global Codex instructions.
35
- - `SoT/.codex/config.toml` pins Codex to `model = "gpt-5.6-sol"`, sets reasoning effort/summaries (`xhigh` + `detailed`), `model_verbosity = "medium"`, `personality`, live top-level `web_search`, workspace-write sandboxing with sandboxed command network access, cross-session `memories` (+ dedicated note tools), `[agents]` subagent limits (`max_threads = 12`, `max_depth = 2` — intentionally above Codex defaults for broad parallel kit work; deeper recursion increases cost and predictability risk), a 128 KiB `project_doc_max_bytes` budget for the repo-side AGENTS.md chain (the global `~/.codex/AGENTS.md` is uncapped and not counted), and enables the Docks plugins as `docks@docks` + `session-relay@docks`.
35
+ - `SoT/.codex/config.toml` pins Codex to `model = "gpt-5.6-sol"`, sets reasoning effort/summaries (`xhigh` + `concise`), `model_verbosity = "low"`, `personality`, live top-level `web_search`, workspace-write sandboxing with sandboxed command network access, cross-session `memories` (+ dedicated note tools), `[agents]` subagent limits (`max_threads = 12`, `max_depth = 2` — intentionally above Codex defaults for broad parallel kit work; deeper recursion increases cost and predictability risk), a 128 KiB `project_doc_max_bytes` budget for the repo-side AGENTS.md chain (the global `~/.codex/AGENTS.md` is uncapped and not counted), and enables the Docks plugins as `docks@docks`, `session-relay@docks`, and `effect-kit@docks`.
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
+ - `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.
38
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.
39
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.
40
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.
@@ -70,6 +72,8 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`), see the matching SoT d
70
72
 
71
73
  Automated coverage includes `bun run test:unit`, `bun run golden:dryrun`, and `bun run golden:mutation`; prove-red modes must exit non-zero after detecting planted mismatches. Also verify user-facing changes via `./docks-kit sync --dry-run`, per-tool sanity (`/doctor`, `/plugin`, etc.), and `diff <(jq -S . <SoT>) <(jq -S . <deployed>)` recipes from the per-tool file.
72
74
 
75
+ Use direct acceptance and focused regressions while iterating, then run the full unit/golden gate once at the pre-commit or release boundary. Reuse still-matching evidence; a later relevant edit invalidates only the affected rung and final gate, not every prior check.
76
+
73
77
  ## Skills
74
78
 
75
79
  This project ships **kit-mechanic skills** under `.claude/skills/` — narrowly-scoped references for how EngineNative works (settings merge, plugin bootstrap, universal-skill install, Codex TOML merge, sync orchestration). They cost prompt tokens only inside this repo's sessions and document regression-prone TypeScript sync logic in `cli/src/engine-native/`. **Pipeline content** (multi-agent slash commands, refactor/security/docs workflows, parallel-scanner agents) belongs in the separate [DocksDocks/docks](https://github.com/DocksDocks/docks) plugin — not here. Project-level agents under `.claude/agents/` follow the same rule: kit-mechanic agents that wrap kit-mechanic skills are permitted; pipeline agents live in the docks plugin.
@@ -83,10 +87,10 @@ When a kit-mechanic skill, its `references/`, or a wrapper agent (`.claude/agent
83
87
  ## Plans
84
88
 
85
89
  <constraint>
86
- 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.
87
91
  </constraint>
88
92
 
89
- 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.
90
94
 
91
95
  Distinct from per-tool **Open Concerns** sections (wait-on-upstream
92
96
  blockers tied to a vendor shipping a fix — these live inside the per-tool
package/README.md CHANGED
@@ -39,7 +39,7 @@ RTK/Bun download boundaries, not as a global sync prerequisite.
39
39
  docks-kit sync [claude] [codex] [agents] deploy the SoT (default: all three)
40
40
  docks-kit update [--no-sync] self-update the kit (autodetects checkout vs global install), then sync
41
41
  docks-kit model <claude|codex> [value] get/set the DEPLOYED model (TTY picker)
42
- docks-kit models [tool] [--json] kit-verified model catalog
42
+ docks-kit models [claude|codex|workflow] model and workflow-role catalogs (`--json`)
43
43
  docks-kit toolchain [check|ensure <tool>] verified-version floors for external tools
44
44
  docks-kit status [--json] deployed-vs-SoT drift + toolchain + counts
45
45
  docks-kit plugins list [--json] enabledPlugins tri-state vs installed
@@ -71,6 +71,21 @@ golden-regression coverage for dry-run output, mutation snapshots, and argv logs
71
71
  and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
72
72
  (includes the old→new rename table for the pre-CLI `sync.sh` flags).
73
73
 
74
+ ### Docks workflow roles (root flags)
75
+
76
+ | Flag | Effect |
77
+ |------|--------|
78
+ | `--model-orchestrator=<selector>` | Override the Docks orchestrator candidate/profile |
79
+ | `--model-reviewer=<selector>` | Override the Docks reviewer candidate/profile |
80
+ | `--model-implementer=<selector>` | Override the Docks implementer candidate/profile |
81
+ | `--review-min-score=<0..100>` | Override the completion-review target |
82
+ | `--review-max-rounds=<1..10>` | Bound completion-review batches |
83
+
84
+ Selectors are strict `profile:<name>` or `<tool>:<model>@<effort>` values from
85
+ `docks-kit models workflow`. These root flags update only the identical compact
86
+ record in `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`; omitted fields retain
87
+ their current valid values, while a later flag-less sync restores all defaults.
88
+
74
89
  ## How syncing works
75
90
 
76
91
  - **Additive by default** — user-only settings keys, plugins, and skills
@@ -83,7 +98,7 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
83
98
  (`--yes` accepts; non-TTY falls back to the pinned verified version
84
99
  when possible). `docks-kit toolchain check` shows the full table.
85
100
  - **Model catalog** — `SoT/models.json` is the research-verified source for
86
- model validation, listings, and pickers.
101
+ model validation, listings, pickers, and the Docks workflow-role registry.
87
102
  - **Claude runtime** — sync materializes three dependency-free Bun `.mjs`
88
103
  programs for statusline, SessionStart, and Notification. Quota display uses
89
104
  Claude's native `rate_limits`; there is no OAuth fetch, shared usage cache,
@@ -97,7 +112,7 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
97
112
  | `SoT/.claude/` | Claude Code SoT (settings template, Bun runtime programs, CLAUDE.md) |
98
113
  | `SoT/.codex/` | Codex SoT (config.toml, rules, AGENTS.md, marketplace) |
99
114
  | `SoT/.agents/` | Universal-skill manifest |
100
- | `SoT/models.json` | Kit-verified model catalog |
115
+ | `SoT/models.json` | Kit-verified model and Docks workflow-role catalog |
101
116
  | `SoT/toolchain.json` | Verified-version floors |
102
117
  | `cli/src/engine-native/` | EngineNative sync/model/toolchain implementation |
103
118
  | `cli/src/generated/sotPayload.ts` | Generated in-memory payload used by standalone and npm installs |
package/cli/docs/flags.md CHANGED
@@ -39,6 +39,23 @@ Bare model, effort, or advisor modifiers print the relevant valid-value catalog
39
39
  and exit 2. A modifier for a target not selected by the positional arguments is
40
40
  ignored with a warning; Claude modifiers never touch Codex config and vice versa.
41
41
 
42
+ ## Docks workflow root flags
43
+
44
+ These flags belong at the root (`docks-kit --model-reviewer=...`), not after
45
+ `sync`. They update only the shared deployed workflow record.
46
+
47
+ | Flag | Accepted value |
48
+ |------|----------------|
49
+ | `--model-orchestrator=<selector>` | `profile:<name>` or `<tool>:<model>@<effort>` |
50
+ | `--model-reviewer=<selector>` | same strict selector grammar |
51
+ | `--model-implementer=<selector>` | same strict selector grammar |
52
+ | `--review-min-score=<n>` | base-10 integer `0..100` |
53
+ | `--review-max-rounds=<n>` | base-10 integer `1..10` |
54
+
55
+ Use `docks-kit models workflow [--json]` for the closed registry. Bare, empty,
56
+ unknown, or malformed values print that helper and exit 2 before either prompt
57
+ file changes.
58
+
42
59
  ## Renamed legacy flags (pre-CLI sync.sh)
43
60
 
44
61
  Old flags exit with a rename hint — there is no compat behavior.
@@ -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
@@ -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
  )
@@ -10,6 +10,7 @@ import {
10
10
  sotCodexModel
11
11
  } from "../manifests"
12
12
  import { kitHome } from "../kitHome"
13
+ import { sessionRelayReadiness } from "../engine-native/sessionRelayReadiness"
13
14
 
14
15
  const json = Options.boolean("json").pipe(
15
16
  Options.withDescription("Machine-readable output")
@@ -47,11 +48,12 @@ export const statusCommand = Command.make("status", { json }, (config) =>
47
48
  const drift = gatherDrift()
48
49
  const plugins = pluginsView()
49
50
  const skills = skillsView()
51
+ const sessionRelay = sessionRelayReadiness()
50
52
  const toolchainTable = yield* engineCapture(["toolchain", "check"])
51
53
 
52
54
  if (config.json) {
53
55
  return yield* Console.log(
54
- JSON.stringify({ kitHome: kitHome(), drift, plugins, skills, toolchainTable }, null, 2)
56
+ JSON.stringify({ kitHome: kitHome(), drift, plugins, skills, sessionRelayReadiness: sessionRelay, toolchainTable }, null, 2)
55
57
  )
56
58
  }
57
59
 
@@ -67,6 +69,11 @@ export const statusCommand = Command.make("status", { json }, (config) =>
67
69
  yield* Console.log(
68
70
  `\nPlugins: ${plugins.length} known (${enabled} SoT-enabled) — details: docks-kit plugins list`
69
71
  )
72
+ yield* Console.log(
73
+ sessionRelay.state === "ready"
74
+ ? `Session Relay: ready for new Codex sessions (v${sessionRelay.version})`
75
+ : `Session Relay: unavailable for new Codex sessions (${sessionRelay.reason})`
76
+ )
70
77
  const installed = skills.filter((s) => s.installed).length
71
78
  yield* Console.log(
72
79
  `Skills: ${skills.length} known (${installed} installed) — details: docks-kit skills list`
@@ -74,6 +81,6 @@ export const statusCommand = Command.make("status", { json }, (config) =>
74
81
  })
75
82
  ).pipe(
76
83
  Command.withDescription(
77
- "Doctor view: deployed-vs-SoT drift (model, effort, compact window), toolchain table, plugin/skill counts."
84
+ "Doctor view: deployed-vs-SoT drift, toolchain, plugin/skill counts, and Session Relay readiness for new Codex sessions."
78
85
  )
79
86
  )
@@ -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
@@ -10,7 +10,9 @@ import { syncCodexEffort, syncCodexModel, replaceTopLevelSettingInFile } from ".
10
10
  import { p } from "./exec"
11
11
  import type { Ctx } from "./index"
12
12
  import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
13
+ import { sessionRelayReadiness } from "./sessionRelayReadiness"
13
14
  import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
15
+ import { renderDefaultWorkflowInstructions } from "./workflowDeploy"
14
16
 
15
17
  export function codexSync(ctx: Ctx): void {
16
18
  const codexDir = p(ctx.home, ".codex")
@@ -23,7 +25,7 @@ export function codexSync(ctx: Ctx): void {
23
25
  syncCodexModel(ctx, ctx.codexModel)
24
26
  syncCodexEffort(ctx, ctx.codexEffort)
25
27
  syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
26
- syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
28
+ syncAgentsMd(ctx, renderDefaultWorkflowInstructions(payloadText("SoT/.codex/AGENTS.md")), p(codexDir, "AGENTS.md"))
27
29
  syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
28
30
  removeLegacyDocksMarketplace(ctx, userConfig)
29
31
  syncPlugins(ctx, sotConfig)
@@ -476,6 +478,10 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
476
478
  if (refreshed > 0) {
477
479
  change(`Codex plugins synced (plugins: ~${refreshed})`)
478
480
  ctx.nextStepTriggers.codexRestart = true
481
+ const readiness = sessionRelayReadiness()
482
+ if (readiness.state !== "ready") {
483
+ warn(`Session Relay readiness unavailable after refresh: ${readiness.reason}`)
484
+ }
479
485
  }
480
486
  if (failed > 0) warn(`${failed} Codex plugin operation(s) failed — re-run sync or install manually`)
481
487
  }
@@ -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"
@@ -124,6 +125,19 @@ function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
124
125
  return 0
125
126
  }
126
127
 
128
+ function engineWorkflow(ctx: Ctx, args: ReadonlyArray<string>): number {
129
+ try {
130
+ const overrides = parseWorkflowArgs(ctx, args)
131
+ deployWorkflowOverrides(ctx, overrides)
132
+ return 0
133
+ } catch (error) {
134
+ if (error instanceof ExitError) throw error
135
+ printWorkflowUsage(ctx)
136
+ ctx.services.logger.err(error instanceof Error ? error.message : String(error))
137
+ throw new ExitError(2)
138
+ }
139
+ }
140
+
127
141
  export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineServices): number {
128
142
  let ctx!: Ctx
129
143
  const baseServices = services ?? makeEngineServices()
@@ -149,6 +163,8 @@ export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineSe
149
163
  return modeModel(ctx, argv.slice(1))
150
164
  case "toolchain":
151
165
  return modeToolchain(ctx, argv.slice(1))
166
+ case "workflow":
167
+ return engineWorkflow(ctx, argv.slice(1))
152
168
  case "sync":
153
169
  return engineSync(ctx, argv.slice(1))
154
170
  default:
@@ -16,6 +16,11 @@ import {
16
16
  isEffortModifierValue
17
17
  } from "../efforts"
18
18
  import { printModels, validateClaudeModel, validateCodexModel } from "./models"
19
+ import {
20
+ buildWorkflowRecord,
21
+ workflowCatalog,
22
+ type WorkflowOverrides
23
+ } from "../workflowModels"
19
24
 
20
25
  export class ExitError extends Error {
21
26
  constructor(readonly code: number) {
@@ -259,6 +264,65 @@ function printCatalog(ctx: Ctx, catalog: string): void {
259
264
  for (const line of catalog.split("\n")) ctx.services.logger.echo(line)
260
265
  }
261
266
 
267
+ const WORKFLOW_FLAGS = {
268
+ "--model-orchestrator": "orchestrator",
269
+ "--model-reviewer": "reviewer",
270
+ "--model-implementer": "implementer",
271
+ "--review-min-score": "minimumScore",
272
+ "--review-max-rounds": "maxRounds"
273
+ } as const satisfies Readonly<Record<string, keyof WorkflowOverrides>>
274
+
275
+ function workflowUsage(ctx: Ctx): void {
276
+ printCatalog(ctx, workflowCatalog())
277
+ ctx.services.logger.echo("")
278
+ ctx.services.logger.echo("Workflow override flags:")
279
+ ctx.services.logger.echo(" --model-orchestrator=<profile:name|tool:model@effort>")
280
+ ctx.services.logger.echo(" --model-reviewer=<profile:name|tool:model@effort>")
281
+ ctx.services.logger.echo(" --model-implementer=<profile:name|tool:model@effort>")
282
+ ctx.services.logger.echo(" --review-min-score=<0..100>")
283
+ ctx.services.logger.echo(" --review-max-rounds=<1..10>")
284
+ }
285
+
286
+ export function parseWorkflowArgs(ctx: Ctx, args: ReadonlyArray<string>): WorkflowOverrides {
287
+ const overrides: Partial<Record<keyof WorkflowOverrides, string>> = {}
288
+ for (let index = 0; index < args.length; index += 1) {
289
+ const arg = args[index] ?? ""
290
+ if (arg === "-h" || arg === "--help") {
291
+ workflowUsage(ctx)
292
+ throw new ExitError(0)
293
+ }
294
+
295
+ const equals = arg.indexOf("=")
296
+ const flag = equals === -1 ? arg : arg.slice(0, equals)
297
+ const key = WORKFLOW_FLAGS[flag as keyof typeof WORKFLOW_FLAGS]
298
+ if (key === undefined) throw new Error(`Unknown workflow arg: ${arg}`)
299
+
300
+ let value: string
301
+ if (equals !== -1) {
302
+ value = arg.slice(equals + 1)
303
+ } else {
304
+ const next = args[index + 1]
305
+ if (next === undefined || next.startsWith("--")) {
306
+ throw new Error(`${flag} requires a value: ${flag}=<value>`)
307
+ }
308
+ value = next
309
+ index += 1
310
+ }
311
+ if (value === "") throw new Error(`${flag} requires a value: ${flag}=<value>`)
312
+ overrides[key] = value
313
+ }
314
+
315
+ if (Object.keys(overrides).length === 0) {
316
+ throw new Error("At least one workflow override flag is required")
317
+ }
318
+ buildWorkflowRecord(overrides)
319
+ return overrides
320
+ }
321
+
322
+ export function printWorkflowUsage(ctx: Ctx): void {
323
+ workflowUsage(ctx)
324
+ }
325
+
262
326
  export function validateModifierFlags(ctx: Ctx): void {
263
327
  const { err, warn } = ctx.services.logger
264
328
  const supplied = (flag: ModifierFlag, value: string): boolean =>
@@ -0,0 +1,94 @@
1
+ import { spawnSync } from "node:child_process"
2
+
3
+ import { isObject, parseJson } from "./jq"
4
+
5
+ const SESSION_RELAY_PLUGIN_ID = "session-relay@docks"
6
+
7
+ export type SessionRelayReadinessReason =
8
+ | "codex_cli_unavailable"
9
+ | "plugin_list_failed"
10
+ | "invalid_plugin_list"
11
+ | "plugin_missing"
12
+ | "plugin_ambiguous"
13
+ | "plugin_not_installed"
14
+ | "plugin_disabled"
15
+
16
+ export interface SessionRelayReadiness {
17
+ readonly schema: 1
18
+ readonly state: "ready" | "unavailable"
19
+ readonly reason: SessionRelayReadinessReason | null
20
+ readonly version: string | null
21
+ readonly installed: boolean
22
+ readonly enabled: boolean
23
+ readonly scope: "new_sessions"
24
+ }
25
+
26
+ export interface CodexPluginListProbe {
27
+ readonly status: number | null
28
+ readonly stdout: string
29
+ readonly errorCode: string | null
30
+ }
31
+
32
+ function unavailable(
33
+ reason: SessionRelayReadinessReason,
34
+ fields: { version?: string | null; installed?: boolean; enabled?: boolean } = {}
35
+ ): SessionRelayReadiness {
36
+ return {
37
+ schema: 1,
38
+ state: "unavailable",
39
+ reason,
40
+ version: fields.version ?? null,
41
+ installed: fields.installed ?? false,
42
+ enabled: fields.enabled ?? false,
43
+ scope: "new_sessions"
44
+ }
45
+ }
46
+
47
+ export function classifySessionRelayReadiness(probe: CodexPluginListProbe): SessionRelayReadiness {
48
+ if (probe.errorCode === "ENOENT") return unavailable("codex_cli_unavailable")
49
+ if (probe.errorCode !== null || probe.status !== 0) return unavailable("plugin_list_failed")
50
+
51
+ const value = parseJson(probe.stdout)
52
+ if (value === undefined || !isObject(value) || !Array.isArray(value["installed"])) {
53
+ return unavailable("invalid_plugin_list")
54
+ }
55
+
56
+ const matches = value["installed"].filter(
57
+ (entry) => isObject(entry) && entry["pluginId"] === SESSION_RELAY_PLUGIN_ID
58
+ )
59
+ if (matches.length === 0) return unavailable("plugin_missing")
60
+ if (matches.length !== 1) return unavailable("plugin_ambiguous")
61
+
62
+ const row = matches[0]
63
+ if (row === undefined || !isObject(row)) return unavailable("invalid_plugin_list")
64
+ const version = row["version"]
65
+ const installed = row["installed"]
66
+ const enabled = row["enabled"]
67
+ if (typeof version !== "string" || version.length === 0 || typeof installed !== "boolean" || typeof enabled !== "boolean") {
68
+ return unavailable("invalid_plugin_list")
69
+ }
70
+ if (!installed) return unavailable("plugin_not_installed", { version, enabled })
71
+ if (!enabled) return unavailable("plugin_disabled", { version, installed })
72
+
73
+ return {
74
+ schema: 1,
75
+ state: "ready",
76
+ reason: null,
77
+ version,
78
+ installed,
79
+ enabled,
80
+ scope: "new_sessions"
81
+ }
82
+ }
83
+
84
+ export function sessionRelayReadiness(): SessionRelayReadiness {
85
+ const result = spawnSync("codex", ["plugin", "list", "--json"], {
86
+ encoding: "utf8",
87
+ stdio: ["ignore", "pipe", "ignore"]
88
+ })
89
+ return classifySessionRelayReadiness({
90
+ status: result.status,
91
+ stdout: result.stdout ?? "",
92
+ errorCode: (result.error as NodeJS.ErrnoException | undefined)?.code ?? null
93
+ })
94
+ }
@@ -0,0 +1,139 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
2
+ import { dirname } from "node:path"
3
+
4
+ import type { Ctx } from "./index"
5
+ import {
6
+ WORKFLOW_RECORD_PREFIX,
7
+ buildWorkflowRecord,
8
+ defaultWorkflowRecord,
9
+ parseWorkflowRecord,
10
+ renderWorkflowRecordLine,
11
+ type WorkflowOverrides,
12
+ type WorkflowRecordV1
13
+ } from "../workflowModels"
14
+
15
+ export interface WorkflowFileSystem {
16
+ readonly read: (path: string) => string | undefined
17
+ readonly writeAtomic: (path: string, bytes: string) => void
18
+ readonly remove: (path: string) => void
19
+ }
20
+
21
+ let atomicWriteSequence = 0
22
+
23
+ const nodeFileSystem: WorkflowFileSystem = {
24
+ read: (path) => existsSync(path) ? readFileSync(path, "utf8") : undefined,
25
+ writeAtomic: (path, bytes) => {
26
+ mkdirSync(dirname(path), { recursive: true })
27
+ const temporary = `${path}.tmp-${process.pid}-${atomicWriteSequence++}`
28
+ try {
29
+ writeFileSync(temporary, bytes)
30
+ renameSync(temporary, path)
31
+ } finally {
32
+ rmSync(temporary, { force: true })
33
+ }
34
+ },
35
+ remove: (path) => rmSync(path, { force: true })
36
+ }
37
+
38
+ interface DocumentRecord {
39
+ readonly record: WorkflowRecordV1 | undefined
40
+ readonly line: string | undefined
41
+ }
42
+
43
+ function readDocumentRecord(document: string | undefined, label: string): DocumentRecord {
44
+ const lines = (document ?? "").split("\n").filter((line) => line.startsWith(WORKFLOW_RECORD_PREFIX))
45
+ if (lines.length === 0) return { record: undefined, line: undefined }
46
+
47
+ let canonicalLine: string | undefined
48
+ let canonicalRecord: WorkflowRecordV1 | undefined
49
+ for (const line of lines) {
50
+ let record: WorkflowRecordV1
51
+ try {
52
+ record = parseWorkflowRecord(JSON.parse(line.slice(WORKFLOW_RECORD_PREFIX.length)) as unknown)
53
+ } catch (error) {
54
+ const detail = error instanceof Error ? error.message : String(error)
55
+ throw new Error(`${label} contains a malformed Docks workflow record: ${detail}`)
56
+ }
57
+ const rendered = renderWorkflowRecordLine(record)
58
+ if (canonicalLine !== undefined && rendered !== canonicalLine) {
59
+ throw new Error(`${label} contains conflicting Docks workflow records`)
60
+ }
61
+ canonicalLine = rendered
62
+ canonicalRecord = record
63
+ }
64
+ return { record: canonicalRecord, line: canonicalLine }
65
+ }
66
+
67
+ export function upsertWorkflowRecord(document: string, record: WorkflowRecordV1): string {
68
+ const content = document
69
+ .split("\n")
70
+ .filter((line) => !line.startsWith(WORKFLOW_RECORD_PREFIX))
71
+ .join("\n")
72
+ .replace(/\n+$/, "")
73
+ const line = renderWorkflowRecordLine(record)
74
+ return content === "" ? `${line}\n` : `${content}\n${line}\n`
75
+ }
76
+
77
+ export function renderDefaultWorkflowInstructions(document: string): string {
78
+ readDocumentRecord(document, "SoT instruction document")
79
+ return upsertWorkflowRecord(document, defaultWorkflowRecord())
80
+ }
81
+
82
+ function restoreSnapshot(fileSystem: WorkflowFileSystem, path: string, snapshot: string | undefined): void {
83
+ if (snapshot === undefined) fileSystem.remove(path)
84
+ else fileSystem.writeAtomic(path, snapshot)
85
+ }
86
+
87
+ export function deployWorkflowOverrides(
88
+ ctx: Ctx,
89
+ overrides: WorkflowOverrides,
90
+ fileSystem: WorkflowFileSystem = nodeFileSystem
91
+ ): void {
92
+ const claudePath = `${ctx.home}/.claude/CLAUDE.md`
93
+ const codexPath = `${ctx.home}/.codex/AGENTS.md`
94
+ const claudeBefore = fileSystem.read(claudePath)
95
+ const codexBefore = fileSystem.read(codexPath)
96
+ const claudeState = readDocumentRecord(claudeBefore, claudePath)
97
+ const codexState = readDocumentRecord(codexBefore, codexPath)
98
+
99
+ if (claudeState.line !== undefined && codexState.line !== undefined && claudeState.line !== codexState.line) {
100
+ throw new Error("Claude and Codex instruction files contain conflicting Docks workflow records")
101
+ }
102
+
103
+ const base = claudeState.record ?? codexState.record ?? defaultWorkflowRecord()
104
+ const record = buildWorkflowRecord(overrides, base)
105
+ const claudeAfter = upsertWorkflowRecord(claudeBefore ?? "", record)
106
+ const codexAfter = upsertWorkflowRecord(codexBefore ?? "", record)
107
+ const claudeChanged = claudeBefore !== claudeAfter
108
+ const codexChanged = codexBefore !== codexAfter
109
+
110
+ if (ctx.dryRun) {
111
+ if (claudeChanged) ctx.services.logger.echo(`[dry-run] update Docks workflow record in ${claudePath}`)
112
+ if (codexChanged) ctx.services.logger.echo(`[dry-run] update Docks workflow record in ${codexPath}`)
113
+ if (!claudeChanged && !codexChanged) ctx.services.logger.verbose("Workflow models already match the requested state")
114
+ return
115
+ }
116
+
117
+ if (!claudeChanged && !codexChanged) {
118
+ ctx.services.logger.verbose("Workflow models already match the requested state")
119
+ return
120
+ }
121
+
122
+ try {
123
+ if (claudeChanged) fileSystem.writeAtomic(claudePath, claudeAfter)
124
+ if (codexChanged) fileSystem.writeAtomic(codexPath, codexAfter)
125
+ } catch (error) {
126
+ try {
127
+ restoreSnapshot(fileSystem, claudePath, claudeBefore)
128
+ restoreSnapshot(fileSystem, codexPath, codexBefore)
129
+ } finally {
130
+ ctx.services.logger.err("Workflow update failed; restored both instruction files to their pre-run state")
131
+ }
132
+ throw error
133
+ }
134
+
135
+ ctx.services.logger.change("Workflow models updated in Claude and Codex instructions")
136
+ ctx.services.logger.warn("Start fresh Claude Code and Codex sessions before relying on the new workflow roles")
137
+ ctx.nextStepTriggers.claudeRestart = true
138
+ ctx.nextStepTriggers.codexRestart = true
139
+ }