docks-kit 0.10.2 → 0.11.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 +4 -6
- package/README.md +3 -20
- package/cli/docs/flags.md +0 -20
- package/cli/docs/models.md +0 -27
- package/cli/docs/modifiers.md +0 -33
- package/cli/src/commands/models.ts +3 -7
- package/cli/src/engine-native/claudeSync.ts +1 -2
- package/cli/src/engine-native/codexSync.ts +1 -2
- package/cli/src/engine-native/index.ts +1 -16
- package/cli/src/engine-native/parseArgs.ts +0 -63
- package/cli/src/generated/sotPayload.ts +5 -5
- package/cli/src/main.ts +3 -54
- package/package.json +1 -1
- package/cli/src/engine-native/workflowDeploy.ts +0 -139
- package/cli/src/workflowModels.ts +0 -452
package/AGENTS.md
CHANGED
|
@@ -20,9 +20,9 @@ launcher can fall back to Bun source.
|
|
|
20
20
|
| Path | Purpose |
|
|
21
21
|
|------|---------|
|
|
22
22
|
| `docks-kit` | CLI launcher: on supported hosts, runs the platform binary in `cli/dist/` only when its `--version` matches `package.json`, otherwise Bun-from-source (auto-installs Bun + `node_modules`). Unsupported hosts fail before source fallback. No-Bun recovery is the standalone platform release binary |
|
|
23
|
-
| `cli/src/engine-native/` | EngineNative implementation for `sync`, `model`,
|
|
23
|
+
| `cli/src/engine-native/` | EngineNative implementation for `sync`, `model`, and `toolchain`; idempotent, flag-gated for destructive reconciliation |
|
|
24
24
|
| `cli/` | Effect-TS CLI + bundled docs topics |
|
|
25
|
-
| `SoT/models.json` | Kit-verified
|
|
25
|
+
| `SoT/models.json` | Kit-verified Claude and Codex model catalog |
|
|
26
26
|
| `SoT/toolchain.json` | Toolchain floors manifest (verified pins consumed by EngineNative) |
|
|
27
27
|
| `SoT/.claude/bin/` | Dependency-free Bun runtime programs for Claude's statusline, SessionStart, and Notification |
|
|
28
28
|
| `install.sh` | Global installer |
|
|
@@ -42,8 +42,6 @@ Codex SoT notes:
|
|
|
42
42
|
- `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.
|
|
43
43
|
- `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.
|
|
44
44
|
- Claude and Codex sync call `sessionRelayCli.ts ensureSessionRelayCli` immediately before their plugin passes. It installs the exact source-pinned precompiled command at `~/.local/bin/session-relay` for Linux/macOS x64/arm64 only, requires committed digest = same-release `SHA256SUMS` row = downloaded bytes, smoke-tests the staged version, and atomically replaces the stable path. `agents`-only sync never enters this boundary. The four pinned production digests correspond to the stable `session-relay--v0.12.0` assets and its `SHA256SUMS`.
|
|
45
|
-
- 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.
|
|
46
|
-
- Codex workflow selectors use the closed `<tool>:<model>@<effort>[+fast]` routing grammar. Fast is intentionally absent from the global SoT: no suffix means Standard and keeps the backward-compatible schema-1 record; `+fast` emits a schema-2 candidate with `service_tier: "fast"`. Docks and Session Relay consumers must explicitly launch unsuffixed Codex roles with the default service tier so a user's global Fast preference cannot leak into workflows.
|
|
47
45
|
- 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.
|
|
48
46
|
- Neither global prompt SoT imports `@RTK.md`; Claude uses the hook-backed RTK integration, while Codex has no kit-managed RTK integration.
|
|
49
47
|
- 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.
|
|
@@ -93,9 +91,9 @@ When a kit-mechanic skill, its `references/`, or a wrapper agent (`.claude/agent
|
|
|
93
91
|
|
|
94
92
|
## Plans
|
|
95
93
|
|
|
96
|
-
|
|
94
|
+
Canonical plans live in `docs/plans/active/`; status is frontmatter and `docs/plans/finished/` is terminal. Exactly three skills are live: `plan-workspace` maintains the workspace; main-context `plan-manager` classifies, drafts, reviews, repairs once, executes, verifies, and archives; internal `plan-reviewer` returns read-only `PlanReviewV1` evidence from one immutable bundle. Only the reviewer has Claude/Codex wrappers.
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
Current plans carry one compact-JCS `Plan-run: PlanRunV1` record. Schemas 1–6 are historical validation/quarantine only and never authorize current dispatch or external effects. Persisted requested effects are intent, not authority; any probe, production access, publish, push, release, or deploy requires matching live authority from the exact current-user request. The full contract lives in `docs/plans/AGENTS.md`.
|
|
99
97
|
|
|
100
98
|
Distinct from per-tool **Open Concerns** sections (wait-on-upstream
|
|
101
99
|
blockers tied to a vendor shipping a fix — these live inside the per-tool
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ RTK/Bun download boundaries, not as a global sync prerequisite.
|
|
|
43
43
|
docks-kit sync [claude] [codex] [agents] deploy the SoT (default: all three)
|
|
44
44
|
docks-kit update [--no-sync] self-update the kit (autodetects checkout vs global install), then sync
|
|
45
45
|
docks-kit model <claude|codex> [value] get/set the DEPLOYED model (TTY picker)
|
|
46
|
-
docks-kit models [claude|codex
|
|
46
|
+
docks-kit models [claude|codex] model catalogs (`--json`)
|
|
47
47
|
docks-kit toolchain [check|ensure <tool>] verified-version floors for external tools
|
|
48
48
|
docks-kit status [--json] deployed-vs-SoT drift + toolchain + counts
|
|
49
49
|
docks-kit plugins list [--json] enabledPlugins tri-state vs installed
|
|
@@ -76,23 +76,6 @@ golden-regression coverage for dry-run output, mutation snapshots, and argv logs
|
|
|
76
76
|
and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
|
|
77
77
|
(includes the old→new rename table for the pre-CLI `sync.sh` flags).
|
|
78
78
|
|
|
79
|
-
### Docks workflow roles (root flags)
|
|
80
|
-
|
|
81
|
-
| Flag | Effect |
|
|
82
|
-
|------|--------|
|
|
83
|
-
| `--model-orchestrator=<selector>` | Override the Docks orchestrator candidate/profile |
|
|
84
|
-
| `--model-reviewer=<selector>` | Override the Docks reviewer candidate/profile |
|
|
85
|
-
| `--model-implementer=<selector>` | Override the Docks implementer candidate/profile |
|
|
86
|
-
| `--review-min-score=<0..100>` | Override the completion-review target |
|
|
87
|
-
| `--review-max-rounds=<1..10>` | Bound completion-review batches |
|
|
88
|
-
|
|
89
|
-
Selectors are strict `profile:<name>` or `<tool>:<model>@<effort>[+fast]`
|
|
90
|
-
values from `docks-kit models workflow`; `+fast` is Codex-only. A selector
|
|
91
|
-
without `+fast` means Standard, while `+fast` writes a schema-2 candidate with
|
|
92
|
-
`service_tier: "fast"`. These root flags update only the identical compact record
|
|
93
|
-
in `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`; omitted fields retain their
|
|
94
|
-
current valid values, while a later flag-less sync restores all defaults.
|
|
95
|
-
|
|
96
79
|
## How syncing works
|
|
97
80
|
|
|
98
81
|
- **Additive by default** — user-only settings keys, plugins, and skills
|
|
@@ -112,7 +95,7 @@ current valid values, while a later flag-less sync restores all defaults.
|
|
|
112
95
|
command. The four production digests are pinned for the stable
|
|
113
96
|
`session-relay--v0.12.0` assets and match the same-release `SHA256SUMS`.
|
|
114
97
|
- **Model catalog** — `SoT/models.json` is the research-verified source for
|
|
115
|
-
model validation, listings,
|
|
98
|
+
model validation, listings, and pickers.
|
|
116
99
|
- **Claude runtime** — sync materializes three dependency-free Bun `.mjs`
|
|
117
100
|
programs for statusline, SessionStart, and Notification. Quota display uses
|
|
118
101
|
Claude's native `rate_limits`; there is no OAuth fetch, shared usage cache,
|
|
@@ -126,7 +109,7 @@ current valid values, while a later flag-less sync restores all defaults.
|
|
|
126
109
|
| `SoT/.claude/` | Claude Code SoT (settings template, Bun runtime programs, CLAUDE.md) |
|
|
127
110
|
| `SoT/.codex/` | Codex SoT (config.toml, rules, AGENTS.md, marketplace) |
|
|
128
111
|
| `SoT/.agents/` | Universal-skill manifest |
|
|
129
|
-
| `SoT/models.json` | Kit-verified
|
|
112
|
+
| `SoT/models.json` | Kit-verified Claude and Codex model catalog |
|
|
130
113
|
| `SoT/toolchain.json` | Verified-version floors |
|
|
131
114
|
| `cli/src/engine-native/` | EngineNative sync/model/toolchain implementation |
|
|
132
115
|
| `cli/src/generated/sotPayload.ts` | Generated in-memory payload used by standalone and npm installs |
|
package/cli/docs/flags.md
CHANGED
|
@@ -40,26 +40,6 @@ Bare model, effort, or advisor modifiers print the relevant valid-value catalog
|
|
|
40
40
|
and exit 2. A modifier for a target not selected by the positional arguments is
|
|
41
41
|
ignored with a warning; Claude modifiers never touch Codex config and vice versa.
|
|
42
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>[+fast]` |
|
|
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. `+fast` is accepted only on Codex exact targets and writes
|
|
59
|
-
`service_tier: "fast"` in workflow record schema 2. Without `+fast`, the role is
|
|
60
|
-
Standard and the record remains schema 1 unless another role is Fast. Shell
|
|
61
|
-
quotes are optional but recommended around selectors containing `+fast`.
|
|
62
|
-
|
|
63
43
|
## Renamed legacy flags (pre-CLI sync.sh)
|
|
64
44
|
|
|
65
45
|
Old flags exit with a rename hint — there is no compat behavior.
|
package/cli/docs/models.md
CHANGED
|
@@ -31,35 +31,8 @@ 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
|
|
36
34
|
```
|
|
37
35
|
|
|
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>[+fast]` targets whose model and effort both appear in
|
|
43
|
-
the catalog. `+fast` is accepted only for Codex exact targets. 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@high`;
|
|
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
|
-
|
|
55
|
-
The default and every unsuffixed selector emit the backward-compatible schema-1
|
|
56
|
-
record with no service-tier field; absence means Standard. A `+fast` selector
|
|
57
|
-
promotes the complete record to schema 2 and adds `service_tier: "fast"` only to
|
|
58
|
-
the selected Codex candidate. Replacing the last Fast selector with an
|
|
59
|
-
unsuffixed selector demotes the record back to schema 1. Docks and Session Relay
|
|
60
|
-
must support schema 2 before `+fast` is deployed, and must explicitly select the
|
|
61
|
-
default tier for unsuffixed roles so global Codex configuration cannot leak in.
|
|
62
|
-
|
|
63
36
|
## Advisor pairing note (Claude)
|
|
64
37
|
|
|
65
38
|
The SoT ships `model: fable` with advisor off (`advisorModel` unset).
|
package/cli/docs/modifiers.md
CHANGED
|
@@ -33,13 +33,6 @@ owns that key for the run: `on` writes `fable`; `off` and `default` delete it.
|
|
|
33
33
|
Codex has no advisor modifier because its documented config has no advisor
|
|
34
34
|
setting; `review_model` applies only to `/review`.
|
|
35
35
|
|
|
36
|
-
Codex Fast mode is intentionally not a global SoT default or per-machine
|
|
37
|
-
modifier. It is role-scoped through the root workflow flags: append `+fast` to a
|
|
38
|
-
Codex exact selector to emit `service_tier: "fast"` in record schema 2. Without
|
|
39
|
-
the suffix, the role means Standard and emits no tier field. Docks and Session
|
|
40
|
-
Relay consumers must map that absence to an explicit default-tier process
|
|
41
|
-
override rather than inherit a user's global Fast preference.
|
|
42
|
-
|
|
43
36
|
## Standalone get/set (no full sync)
|
|
44
37
|
|
|
45
38
|
```
|
|
@@ -52,32 +45,6 @@ docks-kit model codex gpt-5.5 # codex flavor
|
|
|
52
45
|
The set path calls the same engine functions as the sync flags —
|
|
53
46
|
one implementation, two entry points.
|
|
54
47
|
|
|
55
|
-
## Workflow-role overrides (root, no full sync)
|
|
56
|
-
|
|
57
|
-
The root workflow flags are deployed modifiers for Docks rather than one tool:
|
|
58
|
-
|
|
59
|
-
```text
|
|
60
|
-
docks-kit --model-orchestrator=profile:claude-best \
|
|
61
|
-
--model-reviewer="codex:gpt-5.6-sol@high+fast" \
|
|
62
|
-
--model-implementer=codex:gpt-5.6-sol@high \
|
|
63
|
-
--review-min-score=80 --review-max-rounds=5
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
They atomically write one byte-identical complete record to
|
|
67
|
-
`~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`. Omitted fields retain the
|
|
68
|
-
current valid record; a missing side is repaired. Invalid or conflicting state
|
|
69
|
-
stops before mutation, a repeated request is a no-op, and failure on the second
|
|
70
|
-
atomic write restores both snapshots. A later flag-less sync restores the
|
|
71
|
-
embedded defaults. Start fresh sessions after changing the record.
|
|
72
|
-
|
|
73
|
-
Selectors are strict `profile:<name>` or `<tool>:<model>@<effort>[+fast]`
|
|
74
|
-
entries from `docks-kit models workflow`. `+fast` is Codex-only and quotes are
|
|
75
|
-
optional; its absence means Standard. `profile:claude-best` is the ordered Docks
|
|
76
|
-
Fable-high then Opus-xhigh chain; `claude:best@high` is instead Claude's native
|
|
77
|
-
single-model alias. Docks checks each candidate when used—there is no docks-kit
|
|
78
|
-
provider preflight. Install schema-2 support in Docks and Session Relay before
|
|
79
|
-
using a Fast selector.
|
|
80
|
-
|
|
81
48
|
## Sticky opt-ins are NOT modifiers
|
|
82
49
|
|
|
83
50
|
`--claude-plugin=<name>` installs+enables an optional plugin and it STAYS
|
|
@@ -2,10 +2,9 @@ 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"
|
|
6
5
|
|
|
7
6
|
const tool = Args.text({ name: "tool" }).pipe(
|
|
8
|
-
Args.withDescription("claude | codex
|
|
7
|
+
Args.withDescription("claude | codex (omit for both tool catalogs)"),
|
|
9
8
|
Args.optional
|
|
10
9
|
)
|
|
11
10
|
const json = Options.boolean("json").pipe(
|
|
@@ -25,11 +24,8 @@ const renderTool = (t: Tool) =>
|
|
|
25
24
|
export const modelsCommand = Command.make("models", { tool, json }, (config) =>
|
|
26
25
|
Effect.gen(function* () {
|
|
27
26
|
const requested = Option.getOrUndefined(config.tool)
|
|
28
|
-
if (requested === "workflow") {
|
|
29
|
-
return yield* Console.log(config.json ? workflowRegistryJson() : workflowCatalog())
|
|
30
|
-
}
|
|
31
27
|
if (requested !== undefined && requested !== "claude" && requested !== "codex") {
|
|
32
|
-
return yield* bail(`Unknown tool '${requested}' (valid: claude, codex
|
|
28
|
+
return yield* bail(`Unknown tool '${requested}' (valid: claude, codex)`)
|
|
33
29
|
}
|
|
34
30
|
const tools: Array<Tool> = requested !== undefined ? [requested as Tool] : ["claude", "codex"]
|
|
35
31
|
|
|
@@ -46,5 +42,5 @@ export const modelsCommand = Command.make("models", { tool, json }, (config) =>
|
|
|
46
42
|
)
|
|
47
43
|
})
|
|
48
44
|
).pipe(
|
|
49
|
-
Command.withDescription("List kit-verified
|
|
45
|
+
Command.withDescription("List kit-verified Claude and Codex models (SoT/models.json).")
|
|
50
46
|
)
|
|
@@ -31,7 +31,6 @@ 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"
|
|
35
34
|
import { ensureSessionRelayCli } from "./sessionRelayCli"
|
|
36
35
|
|
|
37
36
|
export type ClaudeRuntimeState =
|
|
@@ -207,7 +206,7 @@ function syncClaudeMd(ctx: Ctx, claudeDir: string): void {
|
|
|
207
206
|
return
|
|
208
207
|
}
|
|
209
208
|
|
|
210
|
-
const source =
|
|
209
|
+
const source = payloadText("SoT/.claude/CLAUDE.md")
|
|
211
210
|
if (writeTextIfChanged(p(claudeDir, "CLAUDE.md"), source)) {
|
|
212
211
|
change("CLAUDE.md synced")
|
|
213
212
|
} else {
|
|
@@ -12,7 +12,6 @@ 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"
|
|
16
15
|
import { ensureSessionRelayCli } from "./sessionRelayCli"
|
|
17
16
|
|
|
18
17
|
export function codexSync(ctx: Ctx): void {
|
|
@@ -26,7 +25,7 @@ export function codexSync(ctx: Ctx): void {
|
|
|
26
25
|
syncCodexModel(ctx, ctx.codexModel)
|
|
27
26
|
syncCodexEffort(ctx, ctx.codexEffort)
|
|
28
27
|
syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
|
|
29
|
-
syncAgentsMd(ctx,
|
|
28
|
+
syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
|
|
30
29
|
syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
|
|
31
30
|
removeLegacyDocksMarketplace(ctx, userConfig)
|
|
32
31
|
ensureSessionRelayCli(ctx)
|
|
@@ -15,8 +15,7 @@ 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,
|
|
19
|
-
import { deployWorkflowOverrides } from "./workflowDeploy"
|
|
18
|
+
import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
|
|
20
19
|
|
|
21
20
|
export type ModifierFlag =
|
|
22
21
|
| "--claude-model"
|
|
@@ -127,18 +126,6 @@ function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
127
126
|
return 0
|
|
128
127
|
}
|
|
129
128
|
|
|
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
129
|
|
|
143
130
|
export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineServices): number {
|
|
144
131
|
let ctx!: Ctx
|
|
@@ -165,8 +152,6 @@ export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineSe
|
|
|
165
152
|
return modeModel(ctx, argv.slice(1))
|
|
166
153
|
case "toolchain":
|
|
167
154
|
return modeToolchain(ctx, argv.slice(1))
|
|
168
|
-
case "workflow":
|
|
169
|
-
return engineWorkflow(ctx, argv.slice(1))
|
|
170
155
|
case "sync":
|
|
171
156
|
return engineSync(ctx, argv.slice(1))
|
|
172
157
|
default:
|
|
@@ -16,11 +16,6 @@ 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"
|
|
24
19
|
|
|
25
20
|
export class ExitError extends Error {
|
|
26
21
|
constructor(readonly code: number) {
|
|
@@ -268,64 +263,6 @@ function printCatalog(ctx: Ctx, catalog: string): void {
|
|
|
268
263
|
for (const line of catalog.split("\n")) ctx.services.logger.echo(line)
|
|
269
264
|
}
|
|
270
265
|
|
|
271
|
-
const WORKFLOW_FLAGS = {
|
|
272
|
-
"--model-orchestrator": "orchestrator",
|
|
273
|
-
"--model-reviewer": "reviewer",
|
|
274
|
-
"--model-implementer": "implementer",
|
|
275
|
-
"--review-min-score": "minimumScore",
|
|
276
|
-
"--review-max-rounds": "maxRounds"
|
|
277
|
-
} as const satisfies Readonly<Record<string, keyof WorkflowOverrides>>
|
|
278
|
-
|
|
279
|
-
function workflowUsage(ctx: Ctx): void {
|
|
280
|
-
printCatalog(ctx, workflowCatalog())
|
|
281
|
-
ctx.services.logger.echo("")
|
|
282
|
-
ctx.services.logger.echo("Workflow override flags:")
|
|
283
|
-
ctx.services.logger.echo(" --model-orchestrator=<profile:name|tool:model@effort[+fast]>")
|
|
284
|
-
ctx.services.logger.echo(" --model-reviewer=<profile:name|tool:model@effort[+fast]>")
|
|
285
|
-
ctx.services.logger.echo(" --model-implementer=<profile:name|tool:model@effort[+fast]>")
|
|
286
|
-
ctx.services.logger.echo(" --review-min-score=<0..100>")
|
|
287
|
-
ctx.services.logger.echo(" --review-max-rounds=<1..10>")
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function parseWorkflowArgs(ctx: Ctx, args: ReadonlyArray<string>): WorkflowOverrides {
|
|
291
|
-
const overrides: Partial<Record<keyof WorkflowOverrides, string>> = {}
|
|
292
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
293
|
-
const arg = args[index] ?? ""
|
|
294
|
-
if (arg === "-h" || arg === "--help") {
|
|
295
|
-
workflowUsage(ctx)
|
|
296
|
-
throw new ExitError(0)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const equals = arg.indexOf("=")
|
|
300
|
-
const flag = equals === -1 ? arg : arg.slice(0, equals)
|
|
301
|
-
const key = WORKFLOW_FLAGS[flag as keyof typeof WORKFLOW_FLAGS]
|
|
302
|
-
if (key === undefined) throw new Error(`Unknown workflow arg: ${arg}`)
|
|
303
|
-
|
|
304
|
-
let value: string
|
|
305
|
-
if (equals !== -1) {
|
|
306
|
-
value = arg.slice(equals + 1)
|
|
307
|
-
} else {
|
|
308
|
-
const next = args[index + 1]
|
|
309
|
-
if (next === undefined || next.startsWith("--")) {
|
|
310
|
-
throw new Error(`${flag} requires a value: ${flag}=<value>`)
|
|
311
|
-
}
|
|
312
|
-
value = next
|
|
313
|
-
index += 1
|
|
314
|
-
}
|
|
315
|
-
if (value === "") throw new Error(`${flag} requires a value: ${flag}=<value>`)
|
|
316
|
-
overrides[key] = value
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (Object.keys(overrides).length === 0) {
|
|
320
|
-
throw new Error("At least one workflow override flag is required")
|
|
321
|
-
}
|
|
322
|
-
buildWorkflowRecord(overrides)
|
|
323
|
-
return overrides
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export function printWorkflowUsage(ctx: Ctx): void {
|
|
327
|
-
workflowUsage(ctx)
|
|
328
|
-
}
|
|
329
266
|
|
|
330
267
|
export function validateModifierFlags(ctx: Ctx): void {
|
|
331
268
|
const { err, warn } = ctx.services.logger
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// Generated by cli/scripts/generate-sot-payload.ts. DO NOT EDIT.
|
|
2
2
|
// Edit SoT/, notification.mp3, or package.json, then run: bun cli/scripts/generate-sot-payload.ts
|
|
3
3
|
|
|
4
|
-
export const GENERATED_PACKAGE_VERSION = "0.
|
|
4
|
+
export const GENERATED_PACKAGE_VERSION = "0.11.0"
|
|
5
5
|
|
|
6
6
|
export const GENERATED_PAYLOAD_TEXT = {
|
|
7
7
|
"SoT/.agents/skills.txt": "# Universal AI-agent skill manifest intentionally empty.\n# Global skill discovery is opt-in: add one <owner>/<repo> slug per line.\n# EngineNative ignores comments and blank lines.\n",
|
|
8
|
-
"SoT/models.json": "{\n \"$comment\": \"Kit-verified model catalog — single source for EngineNative validators, the docks-kit CLI (models/model commands,
|
|
8
|
+
"SoT/models.json": "{\n \"$comment\": \"Kit-verified model catalog — single source for EngineNative validators, the docks-kit CLI (models/model commands, pickers, and bare-flag help), and docs. Entries are research-proofed: update an entry and its tool-level `verified` date when a model ships or retires. Deploy-time model flags remain permissive.\",\n \"claude\": {\n \"verified\": \"2026-07-22\",\n \"models\": [\n { \"id\": \"best\", \"kind\": \"alias\", \"note\": \"Fable 5 where the org has access, latest Opus otherwise (Claude Code >=2.1.170)\" },\n { \"id\": \"opus\", \"kind\": \"alias\", \"note\": \"latest Opus (currently Opus 4.8)\" },\n { \"id\": \"fable\", \"kind\": \"alias\", \"note\": \"Fable 5 — the kit SoT default; needs org access + Claude Code >=2.1.170\" },\n { \"id\": \"sonnet\", \"kind\": \"alias\", \"note\": \"latest Sonnet (currently Sonnet 5)\" },\n { \"id\": \"haiku\", \"kind\": \"alias\", \"note\": \"latest Haiku (currently Haiku 4.5)\" },\n { \"id\": \"default\", \"kind\": \"alias\", \"note\": \"engine pseudo-value: deletes the deployed model key so the account default applies\" },\n { \"id\": \"claude-fable-5\", \"kind\": \"id\", \"note\": \"Fable 5\" },\n { \"id\": \"claude-opus-4-8\", \"kind\": \"id\", \"note\": \"Opus 4.8\" },\n { \"id\": \"claude-sonnet-5\", \"kind\": \"id\", \"note\": \"Sonnet 5\" },\n { \"id\": \"claude-haiku-4-5-20251001\", \"kind\": \"id\", \"note\": \"Haiku 4.5\" }\n ]\n },\n \"codex\": {\n \"verified\": \"2026-07-16\",\n \"models\": [\n { \"id\": \"gpt-5.6-sol\", \"kind\": \"id\", \"note\": \"GPT-5.6 Sol — frontier, recommended default; the kit SoT pin\" },\n { \"id\": \"gpt-5.6-terra\", \"kind\": \"id\", \"note\": \"GPT-5.6 Terra — balanced tier\" },\n { \"id\": \"gpt-5.6-luna\", \"kind\": \"id\", \"note\": \"GPT-5.6 Luna — fast/light tier\" },\n { \"id\": \"gpt-5.5\", \"kind\": \"id\", \"note\": \"previous generation\" },\n { \"id\": \"gpt-5.5-codex\", \"kind\": \"id\", \"note\": \"codex-tuned gpt-5.5\" },\n { \"id\": \"gpt-5.1\", \"kind\": \"id\", \"note\": \"previous generation\" },\n { \"id\": \"gpt-5\", \"kind\": \"id\", \"note\": \"previous generation\" },\n { \"id\": \"gpt-5-codex\", \"kind\": \"id\", \"note\": \"codex-tuned gpt-5\" }\n ]\n }\n}\n",
|
|
9
9
|
"SoT/toolchain.json": "{\n \"$comment\": \"Kit toolchain manifest — DATA only (versions, floors, policy); check/install logic lives in cli/src/engine-native/toolchain.ts with per-surface sync callbacks in cli/src/engine-native/. kind: check (doctor visibility only) | managed (kit installs/upgrades it) | managed-release (dedicated source-pinned release installer) | pin (no binary probe — a version pin for a tool the kit invokes via npx). policy (managed only): track (upgrade toward latest, gated by `verified`) | present (install when missing, never upgrade); managed-release uses exact. `verified` = last kit-tested version — anything above it prompts before install (--yes auto-accepts; non-TTY declines and falls back to the pinned `verified` when pinnable). Supply-chain stance: every kit-driven install is pinned to `verified` or gated by it — never floating @latest (npm-worm/Shai-Hulud surface). Update `verified` after testing a new release.\",\n \"tools\": {\n \"jq\": { \"kind\": \"check\", \"note\": \"optional operator CLI; EngineNative JSON and Claude runtime do not invoke it\" },\n \"curl\": { \"kind\": \"check\", \"note\": \"contextual POSIX installer transport for RTK/Bun bootstrap\" },\n \"git\": { \"kind\": \"check\", \"note\": \"plugin marketplaces (claude/codex clone them) + kit checkout updates\" },\n \"node\": { \"kind\": \"check\", \"note\": \"hosts npm globals (agent-browser, LSP servers)\" },\n \"npm\": { \"kind\": \"check\", \"note\": \"npm-global installer\" },\n \"claude\": { \"kind\": \"check\", \"floor\": \"2.1.170\", \"note\": \"kit floor — `best` alias + Fable 5 need >=2.1.170 (mirrors settings minimumVersion)\" },\n \"codex\": { \"kind\": \"check\", \"note\": \"upstream-owned; standalone installer prints when missing\" },\n \"ffplay\": { \"kind\": \"check\", \"note\": \"Notification hook sound (previously unchecked)\" },\n \"bwrap\": { \"kind\": \"check\", \"os\": \"linux\", \"note\": \"Codex Linux sandbox runtime\" },\n \"intelephense\": { \"kind\": \"check\", \"verified\": \"1.18.5\", \"note\": \"php-lsp server binary; `verified` pins claude::sync_lsp_servers' npm install\" },\n \"typescript-language-server\": { \"kind\": \"check\", \"verified\": \"5.3.0\", \"note\": \"typescript-lsp server binary; `verified` pins claude::sync_lsp_servers' npm install\" },\n \"tsc\": { \"kind\": \"check\", \"verified\": \"6.0.3\", \"note\": \"typescript-lsp dependency (npm package `typescript`); `verified` pins claude::sync_lsp_servers' npm install. Deliberately on the 6.x line: typescript-language-server embeds TypeScript's programmatic API, which TS7 (native) doesn't yet expose — the repo's own devDependency runs TS7 for tsc --noEmit\" },\n \"rtk\": { \"kind\": \"managed\", \"policy\": \"track\", \"floor\": \"0.43.0\", \"verified\": \"0.43.0\", \"pinnable\": true,\n \"note\": \"PreToolUse hook — supply-chain review before unverified upgrades; installer honors RTK_VERSION=vX.Y.Z pin\" },\n \"bun\": { \"kind\": \"managed\", \"policy\": \"present\", \"verified\": \"1.3.14\", \"pinnable\": true,\n \"note\": \"runtime for effect-solutions + the docks-kit CLI; bootstrap installs the verified release (installer takes bun-vX.Y.Z); self-updates via `bun upgrade` when wanted\" },\n \"effect-solutions\": { \"kind\": \"managed\", \"policy\": \"track\", \"verified\": \"0.5.3\", \"pinnable\": true,\n \"note\": \"Effect docs CLI (bun global) — track keeps it self-upgrading, gated by the verified pin\" },\n \"agent-browser\": { \"kind\": \"managed\", \"policy\": \"track\", \"verified\": \"0.32.0\", \"pinnable\": true,\n \"note\": \"browser-automation CLI (npm global), gated by the verified pin; first install also downloads Chrome for Testing\" },\n \"session-relay\": { \"kind\": \"managed-release\", \"policy\": \"exact\", \"verified\": \"0.13.0\", \"repository\": \"DocksDocks/docks\", \"tag\": \"session-relay--v0.13.0\", \"plugin_id\": \"session-relay@docks\", \"plugin_version\": \"0.13.0\", \"install_path\": \"~/.local/bin/session-relay\",\n \"assets\": {\n \"x86_64-unknown-linux-musl\": \"f8c6374c2c704f48135cd646028fbd9e53fd43f9800b4a255fa36a0818744b7b\",\n \"aarch64-unknown-linux-musl\": \"6ebc6d9a38a8c3d1f191647d3ab679d56b69cffba36c3bc3c8eb99b0e163852e\",\n \"x86_64-apple-darwin\": \"06c046182922c6897e81278fecd7280008fa8040a489910993283017101f1be3\",\n \"aarch64-apple-darwin\": \"0686e68e3a88dd0dee647fc18211e941dd0d8012818d0bcfb79fac142b5baf21\"\n } },\n \"skills-cli\": { \"kind\": \"pin\", \"verified\": \"1.5.15\",\n \"note\": \"the `skills` npm package the kit runs via `npx skills@<verified>` on every agents sync (universal-skill install/remove) — pinned, never @latest\" }\n }\n}\n",
|
|
10
|
-
"SoT/.claude/CLAUDE.md": "## Research Before Implementation\n\nBefore writing or modifying code that uses an API, hook, method, or config surface you have not verified in this session, research current documentation first.\n\nResearch workflow:\n1. Prefer official documentation and primary sources for the specific library, framework, or API.\n2. If a local docs or MCP tool is available, use it before broad web search.\n3. Only then proceed to implementation.\n\nResearch when:\n- Installing or configuring a dependency.\n- Using an API, hook, method, or pattern not verified in this session.\n- Upgrading or migrating between versions.\n- Any task where relying on memory could cause stale syntax or behavior.\n\nDo not:\n- Assume API signatures, method names, or config options from memory.\n- Generate framework code without checking current docs first.\n- Skip research because the library seems familiar.\n\n<constraint>\nResearch the codebase before editing. Never change code you have not read.\n</constraint>\n\n## Agentic Harness Heuristics\n\n**1. Persistence.** Keep going until the user's query is completely resolved. Only yield when sure the problem is solved. Before ending a turn, check the last paragraph: if it is a plan, a question you can answer yourself, or a promise of work not done (\"I'll…\"), do that work now.\n\n**2. Default to parallel.** Whenever you have multiple independent operations (reads, greps, web fetches, independent edits), invoke them in a single response with multiple tool-use blocks. Sequential calls only when output of one operation is required as input to the next.\n\n**3. Multi-pass search.** First-pass search often misses — vary the wording (colleague-questions over keywords) before concluding something doesn't exist.\n\n**4. Trace symbols.** Before modifying a symbol, trace it to its definitions and all usages. Don't assume a function's behavior or a type's shape from the call site alone.\n\n**5. Linter-loop 3-strike rule.** Don't loop more than 3 times fixing linter errors on the same file. On the third attempt, stop and ask the user — repeated failure usually means the diagnosis is wrong, not the code.\n\n**6. Read-before-Edit TTL.** If you haven't read a file with the Read tool in the last ~5 messages, re-read it before editing. Cached file content goes stale silently when the user edits between turns.\n\n**7. Big-file rule.** For files >1000 lines, prefer Grep + scoped Read (`offset` + `limit`) over reading the entire file. Whole-file reads bloat context; targeted reads keep the working set small.\n\n**8. Todo hygiene.** Use TaskCreate for items with meaningful outcome (≥5 min, distinct deliverable). Never include operational sub-actions (linting, testing, searching, examining the codebase) as their own todos — those are sub-steps in service of higher-level tasks. Mark complete immediately when done, never in batches.\n\n**9. Literal-instruction rule.** Current frontier models follow instructions literally — they do not silently generalize from intent. Phrase requirements as explicit checklists with success criteria, not narrative.\n\n**10. Context hygiene.** Prefer `/clear` at task boundaries and `/rewind` for wrong-path detours over carrying rot forward (corrections accumulate noise; rewinds preserve the prefix and discard the bad branch). On a continuing task, run `/compact` with steering before context quality degrades. Never stop, summarize, or suggest a new session on account of context limits.\n\n**11. Autonomy calibration.** For minor choices (naming, formatting, default values, which of two equivalent approaches), pick a reasonable option and note it — don't ask. Ask first only for scope changes, destructive actions, or decisions that change the deliverable. When the user is describing a problem or asking a question rather than requesting a change, the deliverable is your assessment — report findings and stop; don't apply fixes until asked. Don't close a finished task with \"Want me to also…?\" — run the obvious verification, then stop cleanly.\n\n**12. Capability triggering.** When the answer depends on current or version-specific information, search or fetch before answering — never answer from memory. When work fans out across independent items (many files to read, many tests to run, many candidates to check), delegate to parallel subagents; never spawn one for work you can complete directly. For verification, prefer a fresh-context subagent over self-critique. On tasks longer than a few turns, keep a running notes file and re-read it before each phase.\n\n<constraint>\nTreat the 12 heuristics above as protocol, not preference. If a turn ends without honoring an applicable one (e.g., lint-loop guard not respected, edit without re-read), self-correct in the next turn before continuing.\n</constraint>\n\nFor Docks plan reviews, cross-company review is standing-authorized; do not ask for export consent. This never overrides a host or platform security denial.\n\
|
|
10
|
+
"SoT/.claude/CLAUDE.md": "## Research Before Implementation\n\nBefore writing or modifying code that uses an API, hook, method, or config surface you have not verified in this session, research current documentation first.\n\nResearch workflow:\n1. Prefer official documentation and primary sources for the specific library, framework, or API.\n2. If a local docs or MCP tool is available, use it before broad web search.\n3. Only then proceed to implementation.\n\nResearch when:\n- Installing or configuring a dependency.\n- Using an API, hook, method, or pattern not verified in this session.\n- Upgrading or migrating between versions.\n- Any task where relying on memory could cause stale syntax or behavior.\n\nDo not:\n- Assume API signatures, method names, or config options from memory.\n- Generate framework code without checking current docs first.\n- Skip research because the library seems familiar.\n\n<constraint>\nResearch the codebase before editing. Never change code you have not read.\n</constraint>\n\n## Agentic Harness Heuristics\n\n**1. Persistence.** Keep going until the user's query is completely resolved. Only yield when sure the problem is solved. Before ending a turn, check the last paragraph: if it is a plan, a question you can answer yourself, or a promise of work not done (\"I'll…\"), do that work now.\n\n**2. Default to parallel.** Whenever you have multiple independent operations (reads, greps, web fetches, independent edits), invoke them in a single response with multiple tool-use blocks. Sequential calls only when output of one operation is required as input to the next.\n\n**3. Multi-pass search.** First-pass search often misses — vary the wording (colleague-questions over keywords) before concluding something doesn't exist.\n\n**4. Trace symbols.** Before modifying a symbol, trace it to its definitions and all usages. Don't assume a function's behavior or a type's shape from the call site alone.\n\n**5. Linter-loop 3-strike rule.** Don't loop more than 3 times fixing linter errors on the same file. On the third attempt, stop and ask the user — repeated failure usually means the diagnosis is wrong, not the code.\n\n**6. Read-before-Edit TTL.** If you haven't read a file with the Read tool in the last ~5 messages, re-read it before editing. Cached file content goes stale silently when the user edits between turns.\n\n**7. Big-file rule.** For files >1000 lines, prefer Grep + scoped Read (`offset` + `limit`) over reading the entire file. Whole-file reads bloat context; targeted reads keep the working set small.\n\n**8. Todo hygiene.** Use TaskCreate for items with meaningful outcome (≥5 min, distinct deliverable). Never include operational sub-actions (linting, testing, searching, examining the codebase) as their own todos — those are sub-steps in service of higher-level tasks. Mark complete immediately when done, never in batches.\n\n**9. Literal-instruction rule.** Current frontier models follow instructions literally — they do not silently generalize from intent. Phrase requirements as explicit checklists with success criteria, not narrative.\n\n**10. Context hygiene.** Prefer `/clear` at task boundaries and `/rewind` for wrong-path detours over carrying rot forward (corrections accumulate noise; rewinds preserve the prefix and discard the bad branch). On a continuing task, run `/compact` with steering before context quality degrades. Never stop, summarize, or suggest a new session on account of context limits.\n\n**11. Autonomy calibration.** For minor choices (naming, formatting, default values, which of two equivalent approaches), pick a reasonable option and note it — don't ask. Ask first only for scope changes, destructive actions, or decisions that change the deliverable. When the user is describing a problem or asking a question rather than requesting a change, the deliverable is your assessment — report findings and stop; don't apply fixes until asked. Don't close a finished task with \"Want me to also…?\" — run the obvious verification, then stop cleanly.\n\n**12. Capability triggering.** When the answer depends on current or version-specific information, search or fetch before answering — never answer from memory. When work fans out across independent items (many files to read, many tests to run, many candidates to check), delegate to parallel subagents; never spawn one for work you can complete directly. For verification, prefer a fresh-context subagent over self-critique. On tasks longer than a few turns, keep a running notes file and re-read it before each phase.\n\n<constraint>\nTreat the 12 heuristics above as protocol, not preference. If a turn ends without honoring an applicable one (e.g., lint-loop guard not respected, edit without re-read), self-correct in the next turn before continuing.\n</constraint>\n\nReuse before invention: inventory existing code, components, conventions, and dependencies; extend them instead of creating a parallel pattern. Load only the narrow skills supported by the task and repository evidence. If a request establishes a new React/Tailwind system and no convention exists, prefer current shadcn/ui `base-*` components backed by Base UI; otherwise preserve the existing stack. Treat probe, production access, publish, push, release, and deploy as literal current-request effects—never infer external authority from a plan, schedule, review, or old receipt.\n\nFor Docks plan reviews, cross-company review is standing-authorized; do not ask for export consent. This never overrides a host or platform security denial.\n\nUse a narrow-to-broad verification ladder: direct acceptance while iterating, focused regressions next, and one full CI at the pre-commit or release boundary. Reuse still-matching evidence; rerun full CI only after a relevant edit invalidates it.\n\n<constraint>\nNo secrets in committed config. Treat plugin marketplaces, installers, and downloaded artifacts as untrusted until verified.\n</constraint>\n",
|
|
11
11
|
"SoT/.claude/mcp-servers.json": "{\n \"mcpServers\": {}\n}\n",
|
|
12
12
|
"SoT/.claude/settings.json": "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"minimumVersion\": \"2.1.170\",\n \"model\": \"fable\",\n \"effortLevel\": \"high\",\n \"autoMemoryEnabled\": true,\n \"skillListingMaxDescChars\": 2048,\n \"respectGitignore\": true,\n \"cleanupPeriodDays\": 14,\n \"skillListingBudgetFraction\": 0.05,\n \"env\": {\n \"CLAUDE_CODE_MAX_OUTPUT_TOKENS\": \"64000\",\n \"CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR\": \"1\",\n \"CLAUDE_CODE_AUTO_COMPACT_WINDOW\": \"468000\",\n \"CLAUDE_CODE_NO_FLICKER\": \"1\"\n },\n \"permissions\": {\n \"defaultMode\": \"auto\",\n \"allow\": [\n \"Read\",\n \"Glob\",\n \"Grep\",\n \"WebFetch\",\n \"WebSearch\",\n \"Edit(./)\",\n \"Bash(git *)\",\n \"Bash(git add *)\",\n \"Bash(git commit *)\",\n \"Bash(git status *)\",\n \"Bash(git diff *)\",\n \"Bash(git log *)\",\n \"Bash(git branch *)\",\n \"Bash(git checkout *)\",\n \"Bash(git switch *)\",\n \"Bash(git stash *)\",\n \"Bash(git fetch *)\",\n \"Bash(git pull *)\",\n \"Bash(git tag *)\",\n \"Bash(git show *)\",\n \"Bash(git blame *)\",\n \"Bash(git worktree *)\",\n \"Bash(gh *)\",\n \"Bash(pnpm *)\",\n \"Bash(npm *)\",\n \"Bash(npx *)\",\n \"Bash(node *)\",\n \"Bash(docker *)\",\n \"Bash(docker-compose *)\",\n \"Bash(rtk *)\",\n \"Bash(ls *)\",\n \"Bash(cat *)\",\n \"Bash(find *)\",\n \"Bash(grep *)\",\n \"Bash(head *)\",\n \"Bash(tail *)\",\n \"Bash(wc *)\",\n \"Bash(sort *)\",\n \"Bash(uniq *)\",\n \"Bash(diff *)\",\n \"Bash(which *)\",\n \"Bash(pwd *)\",\n \"Bash(date *)\",\n \"Bash(mkdir *)\",\n \"Bash(basename *)\",\n \"Bash(dirname *)\",\n \"Bash(realpath *)\",\n \"Bash(jq *)\",\n \"Bash(curl *)\",\n \"Bash(tree *)\",\n \"Bash(sed *)\",\n \"Bash(awk *)\",\n \"Bash(cut *)\",\n \"Bash(tr *)\",\n \"Bash(tee *)\",\n \"Bash(echo *)\",\n \"Bash(printf *)\",\n \"Bash(env *)\",\n \"Bash(printenv *)\",\n \"Bash(uname *)\",\n \"Bash(file *)\",\n \"Bash(stat *)\",\n \"Bash(du *)\",\n \"Bash(id *)\",\n \"Bash(whoami *)\",\n \"Bash(php *)\",\n \"Bash(composer *)\",\n \"Bash(python3 *)\",\n \"Bash(python *)\",\n \"Bash(pip *)\",\n \"Bash(pip3 *)\"\n ],\n \"deny\": [\n \"Read(**/.env)\",\n \"Read(**/.env.local)\",\n \"Read(**/secrets/**)\",\n \"Read(**/*.key)\",\n \"Read(**/*.pem)\",\n \"Read(**/*.p12)\",\n \"Read(**/.credentials*)\",\n \"Edit(**/.env)\",\n \"Edit(**/.env.local)\",\n \"Edit(**/secrets/**)\",\n \"Bash(sudo *)\",\n \"Bash(rm -rf /)\",\n \"Bash(rm -rf / *)\",\n \"Bash(rm -rf ~)\",\n \"Bash(rm -rf ~ *)\",\n \"Bash(rm -rf $HOME)\",\n \"Bash(rm -rf $HOME *)\",\n \"Bash(> /dev *)\",\n \"Bash(dd if= *)\",\n \"Bash(mkfs *)\",\n \"Bash(eval *)\",\n \"Bash(chmod 777 *)\",\n \"Bash(chmod -R 777 *)\",\n \"Bash(git push --force origin main *)\",\n \"Bash(git push --force origin master *)\",\n \"Bash(git push -f origin main *)\",\n \"Bash(git push -f origin master *)\"\n ],\n \"ask\": [\n \"Bash(git clean *)\",\n \"Bash(docker volume rm *)\",\n \"Bash(docker system prune *)\"\n ]\n },\n \"hooks\": {\n \"SessionStart\": [\n {\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"__DOCKS_KIT_BUN__\",\n \"args\": [\"__DOCKS_KIT_SESSION_START__\"],\n \"timeout\": 5\n }\n ]\n }\n ],\n \"Notification\": [\n {\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"__DOCKS_KIT_BUN__\",\n \"args\": [\"__DOCKS_KIT_NOTIFY__\"],\n \"timeout\": 10,\n \"async\": true\n }\n ]\n }\n ],\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"rtk hook claude\"\n }\n ]\n }\n ],\n \"PostToolUseFailure\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"echo '{\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PostToolUseFailure\\\",\\\"additionalContext\\\":\\\"Last bash command failed. Repository / file state may have shifted \\u2014 re-read affected files before retrying. If the failure is a missing dependency or env mismatch, surface it to the user rather than retrying blindly.\\\"}}'\",\n \"timeout\": 5\n }\n ]\n }\n ],\n \"SubagentStop\": [\n {\n \"hooks\": [\n {\n \"type\": \"prompt\",\n \"prompt\": \"You are a quality gate for subagent outputs in a multi-agent code-analysis pipeline.\\n\\nEvaluate the subagent's `last_assistant_message` field (in the JSON below) against these requirements:\\n\\n1. ALLOW (return `{}`): Mode-selection or no-issues responses. Examples: \\\"Which mode do you prefer\\\", \\\"select an option\\\", \\\"no issues / problems / violations / blockers found\\\".\\n\\n2. ALLOW (return `{}`): Output contains at least one concrete file:line citation \\u2014 e.g. `src/auth.ts:42`, `lib/db.ts:100-115`, or path references that include line numbers.\\n\\n3. BLOCK (return `{\\\"decision\\\":\\\"block\\\",\\\"reason\\\":\\\"<one-line explanation>\\\"}`): Output claims about code or findings WITHOUT concrete file:line citations. Vague references like \\\"the auth handler\\\" or \\\"near the database code\\\" are not acceptable as the only evidence.\\n\\nSubagent invocation JSON:\\n$ARGUMENTS\\n\\nReturn ONLY the JSON decision (no commentary, no markdown fences).\",\n \"timeout\": 30\n }\n ]\n }\n ]\n },\n \"statusLine\": {\n \"type\": \"command\",\n \"command\": \"__DOCKS_KIT_STATUSLINE__\",\n \"refreshInterval\": 5\n },\n \"enabledPlugins\": {\n \"docks@docks\": true,\n \"session-relay@docks\": true,\n \"effect-kit@docks\": true,\n \"php-lsp@claude-plugins-official\": true,\n \"typescript-lsp@claude-plugins-official\": true\n },\n \"extraKnownMarketplaces\": {\n \"docks\": {\n \"source\": {\n \"source\": \"github\",\n \"repo\": \"DocksDocks/docks\"\n }\n }\n },\n \"alwaysThinkingEnabled\": true,\n \"showThinkingSummaries\": true,\n \"viewMode\": \"default\",\n \"theme\": \"dark-daltonized\",\n \"skipDangerousModePermissionPrompt\": true\n}\n",
|
|
13
13
|
"SoT/.claude/bin/statusline.mjs": "const ESC = \"\\x1b[\"\nconst PIPE = `${ESC}90m | ${ESC}0m`\nconst DOT = `${ESC}90m • ${ESC}0m`\nconst DIM = `${ESC}2m${ESC}38;2;156;162;175m`\n\nfunction isRecord(value) {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction finitePercentage(value) {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0 && value <= 100\n ? value\n : undefined\n}\n\nfunction roundHalfEven(value) {\n const lower = Math.floor(value)\n const fraction = value - lower\n if (fraction < 0.5) return lower\n if (fraction > 0.5) return lower + 1\n return lower % 2 === 0 ? lower : lower + 1\n}\n\nfunction pathBasename(path) {\n const parts = path.split(/[\\\\/]+/).filter((part) => part !== \"\")\n return parts.at(-1) ?? \"\"\n}\n\nfunction modelName(input) {\n const model = isRecord(input.model) && typeof input.model.display_name === \"string\"\n ? input.model.display_name\n : \"\"\n const suffix = model.indexOf(\" (\")\n return suffix === -1 ? model : model.slice(0, suffix)\n}\n\nfunction workingDirectory(input, cwd) {\n if (isRecord(input.workspace) && typeof input.workspace.current_dir === \"string\" && input.workspace.current_dir !== \"\") {\n return input.workspace.current_dir\n }\n if (typeof input.cwd === \"string\" && input.cwd !== \"\") return input.cwd\n return cwd\n}\n\nfunction compactWindow(env, total) {\n const raw = env.CLAUDE_CODE_AUTO_COMPACT_WINDOW\n if (typeof raw !== \"string\" || !/^[0-9]+$/.test(raw)) return total\n const parsed = Number(raw)\n return Number.isSafeInteger(parsed) && parsed >= 1000 && parsed < total ? parsed : total\n}\n\nfunction formatTokensK(value) {\n if (value < 1000) return `${value}k`\n if (value % 1000 === 0) return `${value / 1000}M`\n return `${(roundHalfEven(value / 100) / 10).toFixed(1)}M`\n}\n\nfunction contextSegment(input, env) {\n if (!isRecord(input.context_window)) return \"\"\n const used = finitePercentage(input.context_window.used_percentage)\n if (used === undefined) return \"\"\n\n const total = input.context_window.context_window_size\n if (typeof total !== \"number\" || !Number.isFinite(total) || total <= 0) {\n return `${ESC}38;2;130;160;230mctx ${roundHalfEven(used)}%${ESC}0m`\n }\n\n const usedK = roundHalfEven((used / 100) * (total / 1000))\n const effectiveK = Math.trunc(compactWindow(env, total) / 1000)\n if (effectiveK <= 0) return `${ESC}38;2;130;160;230mctx ${roundHalfEven(used)}%${ESC}0m`\n const effectivePercentage = roundHalfEven((usedK / effectiveK) * 100)\n return `${ESC}38;2;130;160;230mctx ${effectivePercentage}%${ESC}0m ${DIM}(${formatTokensK(usedK)}/${formatTokensK(effectiveK)})${ESC}0m`\n}\n\nfunction resetDelta(value, nowMs) {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) return \"\"\n const seconds = Math.trunc(value) - Math.floor(nowMs / 1000)\n if (seconds <= 0) return \"now\"\n const days = Math.trunc(seconds / 86_400)\n if (days > 0) return `${days}d`\n const hours = Math.trunc((seconds % 86_400) / 3_600)\n if (hours > 0) return `${hours}h`\n return `${Math.trunc((seconds % 3_600) / 60)}m`\n}\n\nfunction quotaWindow(value, label, color, nowMs) {\n if (!isRecord(value)) return \"\"\n const used = finitePercentage(value.used_percentage)\n if (used === undefined) return \"\"\n const delta = resetDelta(value.resets_at, nowMs)\n const reset = delta === \"\" ? \"\" : ` ${DIM}(${delta})${ESC}0m`\n return `${ESC}38;2;${color}m${label} ${roundHalfEven(used)}%${ESC}0m${reset}`\n}\n\nfunction decodeStdout(result) {\n const stdout = result?.stdout\n if (stdout === undefined || stdout === null) return \"\"\n return typeof stdout === \"string\" ? stdout : stdout.toString()\n}\n\nfunction resolveBranch(directory, which, spawnSync) {\n const git = which(\"git\")\n if (typeof git !== \"string\" || git === \"\") return \"\"\n const commands = [\n [git, \"-C\", directory, \"symbolic-ref\", \"--short\", \"HEAD\"],\n [git, \"-C\", directory, \"rev-parse\", \"--short\", \"HEAD\"]\n ]\n for (const command of commands) {\n try {\n const result = spawnSync(command, { stdin: \"ignore\", stdout: \"pipe\", stderr: \"ignore\" })\n if (result?.success === true) return decodeStdout(result).trim()\n } catch {\n return \"\"\n }\n }\n return \"\"\n}\n\nexport function formatStatusline(input, options = {}) {\n if (!isRecord(input)) return \"\"\n const env = isRecord(options.env) ? options.env : process.env\n const nowMs = typeof options.nowMs === \"number\" ? options.nowMs : Date.now()\n const cwd = typeof options.cwd === \"string\" ? options.cwd : process.cwd()\n const branch = typeof options.branch === \"string\" ? options.branch : \"\"\n const directory = workingDirectory(input, cwd)\n\n const model = `${ESC}38;5;208m${ESC}1m${modelName(input)}${ESC}22m${ESC}0m`\n const folder = `${ESC}1m${ESC}38;2;76;208;222m${pathBasename(directory)}${ESC}22m${ESC}0m`\n const branchSegment = branch === \"\" ? \"\" : `${DOT}${ESC}1m${ESC}38;2;192;103;222m${branch}${ESC}22m${ESC}0m`\n const context = contextSegment(input, env)\n\n const rateLimits = isRecord(input.rate_limits) ? input.rate_limits : {}\n const fiveHour = quotaWindow(rateLimits.five_hour, \"5h\", \"100;200;200\", nowMs)\n const sevenDay = quotaWindow(rateLimits.seven_day, \"7d\", \"230;180;90\", nowMs)\n const quota = fiveHour === \"\" && sevenDay === \"\"\n ? \"\"\n : `${PIPE}${fiveHour}${fiveHour !== \"\" && sevenDay !== \"\" ? DOT : \"\"}${sevenDay}`\n\n return `${model}${PIPE}${folder}${branchSegment}${context === \"\" ? \"\" : `${PIPE}${context}`}${quota}`\n}\n\nexport async function main(options = {}) {\n const readStdin = options.readStdin ?? (() => Bun.stdin.text())\n const writeStdout = options.writeStdout ?? ((value) => process.stdout.write(value))\n let raw\n try {\n raw = await readStdin()\n } catch {\n return 0\n }\n\n let input\n try {\n input = JSON.parse(raw)\n } catch {\n return 0\n }\n if (!isRecord(input)) return 0\n\n const cwd = typeof options.cwd === \"string\" ? options.cwd : process.cwd()\n const directory = workingDirectory(input, cwd)\n const branch = resolveBranch(\n directory,\n options.which ?? ((name) => Bun.which(name)),\n options.spawnSync ?? ((argv, spawnOptions) => Bun.spawnSync(argv, spawnOptions))\n )\n const output = formatStatusline(input, {\n env: options.env ?? process.env,\n nowMs: options.nowMs ?? Date.now(),\n cwd,\n branch\n })\n if (output !== \"\") writeStdout(`${output}\\n`)\n return 0\n}\n\nif (import.meta.main) process.exit(await main())\n",
|
|
14
14
|
"SoT/.claude/bin/session-start.mjs": "import { readFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\n\nfunction isRecord(value) {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction nonEmpty(value, fallback) {\n return typeof value === \"string\" && value !== \"\" ? value : fallback\n}\n\nfunction pad(value) {\n return String(value).padStart(2, \"0\")\n}\n\nfunction configuredEffort(home, readText) {\n try {\n const parsed = JSON.parse(readText(`${home}/.claude/settings.json`))\n return isRecord(parsed) ? nonEmpty(parsed.effortLevel, \"default\") : \"default\"\n } catch {\n return \"default\"\n }\n}\n\nfunction localZone(now) {\n const part = new Intl.DateTimeFormat(\"en-US\", { timeZoneName: \"short\" })\n .formatToParts(now)\n .find((value) => value.type === \"timeZoneName\")\n return part?.value ?? \"\"\n}\n\nexport function sessionStartLines(options = {}) {\n const env = isRecord(options.env) ? options.env : process.env\n const now = options.now instanceof Date ? options.now : new Date()\n const home = typeof options.home === \"string\" ? options.home : homedir()\n const readText = options.readText ?? ((path) => readFileSync(path, \"utf8\"))\n const weekday = new Intl.DateTimeFormat(\"en-US\", { weekday: \"long\" }).format(now)\n const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`\n const time = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`\n const effort = nonEmpty(env.CLAUDE_CODE_EFFORT_LEVEL, configuredEffort(home, readText))\n const context = env.CLAUDE_CODE_DISABLE_1M_CONTEXT === \"1\" ? \"200K\" : \"1M\"\n const compactWindow = nonEmpty(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW, \"full\")\n const subagent = nonEmpty(env.CLAUDE_CODE_SUBAGENT_MODEL, \"default\")\n return [\n `[CONTEXT] Current date: ${weekday}, ${date} ${time} ${localZone(now)}`,\n `[CONFIG] Context: ${context} | Compact-window: ${compactWindow} | Effort: ${effort} | Thinking: adaptive | Subagent: ${subagent}`\n ]\n}\n\nexport async function main(options = {}) {\n const writeStdout = options.writeStdout ?? ((value) => process.stdout.write(value))\n writeStdout(`${sessionStartLines(options).join(\"\\n\")}\\n`)\n return 0\n}\n\nif (import.meta.main) process.exit(await main())\n",
|
|
15
15
|
"SoT/.claude/bin/notify.mjs": "const DEFAULT_SOUND = `${import.meta.dir}/../notification.mp3`\n\nexport function selectPlayer(options = {}) {\n const platform = options.platform ?? process.platform\n const sound = options.sound ?? DEFAULT_SOUND\n const which = options.which ?? ((name) => Bun.which(name))\n if (platform === \"darwin\") {\n const afplay = which(\"afplay\")\n if (typeof afplay === \"string\" && afplay !== \"\") return [afplay, sound]\n }\n const ffplay = which(\"ffplay\")\n if (typeof ffplay === \"string\" && ffplay !== \"\") {\n return [ffplay, \"-nodisp\", \"-autoexit\", \"-loglevel\", \"quiet\", sound]\n }\n const paplay = which(\"paplay\")\n if (typeof paplay === \"string\" && paplay !== \"\") return [paplay, sound]\n const aplay = which(\"aplay\")\n if (typeof aplay === \"string\" && aplay !== \"\") return [aplay, \"-q\", sound]\n return undefined\n}\n\nexport async function main(options = {}) {\n const sound = options.sound ?? DEFAULT_SOUND\n const fileExists = options.fileExists ?? ((path) => Bun.file(path).exists())\n if (!await fileExists(sound)) return 0\n const command = selectPlayer({ ...options, sound })\n if (command === undefined) return 0\n const spawnSync = options.spawnSync ?? ((argv, spawnOptions) => Bun.spawnSync(argv, spawnOptions))\n const result = spawnSync(command, { stdin: \"ignore\", stdout: \"ignore\", stderr: \"ignore\" })\n return typeof result.exitCode === \"number\" ? result.exitCode : 1\n}\n\nif (import.meta.main) process.exit(await main())\n",
|
|
16
|
-
"SoT/.codex/AGENTS.md": "# AGENTS.md\n\n## Research Before Implementation\n\nBefore writing or modifying code that uses an API, hook, method, or config surface you have not verified in this session, research current documentation first.\n\nResearch workflow:\n1. Prefer official documentation and primary sources for the specific library, framework, or API.\n2. If a local docs or MCP tool is available, use it before broad web search.\n3. Only then proceed to implementation.\n\nResearch when:\n- Installing or configuring a dependency.\n- Using an API, hook, method, or pattern not verified in this session.\n- Upgrading or migrating between versions.\n- Any task where relying on memory could cause stale syntax or behavior.\n\nDo not:\n- Assume API signatures, method names, or config options from memory.\n- Generate framework code without checking current docs first.\n- Skip research because the library seems familiar.\n\n<constraint>\nResearch the codebase before editing. Never change code you have not read.\n</constraint>\n\n## Agentic Harness Heuristics\n\nModel-agnostic operating rules for coding-agent work.\n\n1. Persistence. Keep going until the user's request is actually handled. Only yield when the problem is solved or a concrete blocker is identified. Resolve in the fewest useful tool loops — once you can answer the core request with evidence, answer. Before ending a turn, check the last paragraph: if it is a plan, a question you can answer yourself, or a promise of work not done, do that work now.\n2. Default to parallel. When multiple reads, searches, inspections, or independent checks can run without depending on each other, run them together.\n3. Multi-pass search. First-pass search often misses — vary the wording before concluding something does not exist.\n4. Trace symbols. Before modifying a symbol, trace its definition and usages. Do not infer behavior from one call site.\n5. Linter-loop 3-strike rule. Do not loop more than 3 times fixing the same lint/test failure without reassessing the diagnosis.\n6. Read-before-edit TTL. If you have not read a file recently, re-read it before editing. User edits can make cached context stale.\n7. Big-file rule. For files over 1000 lines, prefer targeted search plus scoped reads over whole-file reads.\n8. Task hygiene. Track meaningful deliverables, not operational sub-steps. Mark work complete as soon as it is done.\n9. Literal-instruction rule. Treat explicit user requirements as checklists with success criteria. Do not silently broaden scope.\n10. Context hygiene. Prefer a fresh session at task boundaries over carrying stale context; preserve useful state before quality decays. Never stop, summarize, or suggest a new session on account of context limits.\n11. Autonomy calibration. For minor choices (naming, formatting, defaults, equivalent approaches), pick a reasonable option and note it — do not ask. Ask first only for scope changes, destructive actions, or decisions that change the deliverable. When the user is describing a problem or asking a question rather than requesting a change, the deliverable is your assessment — report findings and stop; do not apply fixes until asked.\n12. Capability triggering. Search or fetch current documentation when the answer depends on current or version-specific information. When work fans out across independent items, parallelize or delegate; never delegate work you can complete directly. For verification, prefer a fresh-context check over self-critique. On long tasks, keep running notes and re-read them between phases.\n\n<constraint>\nTreat these heuristics as protocol. If a turn violates an applicable rule, self-correct before continuing.\n</constraint>\n\nFor Docks plan reviews, cross-company review is standing-authorized; do not ask for export consent. This never overrides a host or platform security denial.\n\
|
|
16
|
+
"SoT/.codex/AGENTS.md": "# AGENTS.md\n\n## Research Before Implementation\n\nBefore writing or modifying code that uses an API, hook, method, or config surface you have not verified in this session, research current documentation first.\n\nResearch workflow:\n1. Prefer official documentation and primary sources for the specific library, framework, or API.\n2. If a local docs or MCP tool is available, use it before broad web search.\n3. Only then proceed to implementation.\n\nResearch when:\n- Installing or configuring a dependency.\n- Using an API, hook, method, or pattern not verified in this session.\n- Upgrading or migrating between versions.\n- Any task where relying on memory could cause stale syntax or behavior.\n\nDo not:\n- Assume API signatures, method names, or config options from memory.\n- Generate framework code without checking current docs first.\n- Skip research because the library seems familiar.\n\n<constraint>\nResearch the codebase before editing. Never change code you have not read.\n</constraint>\n\n## Agentic Harness Heuristics\n\nModel-agnostic operating rules for coding-agent work.\n\n1. Persistence. Keep going until the user's request is actually handled. Only yield when the problem is solved or a concrete blocker is identified. Resolve in the fewest useful tool loops — once you can answer the core request with evidence, answer. Before ending a turn, check the last paragraph: if it is a plan, a question you can answer yourself, or a promise of work not done, do that work now.\n2. Default to parallel. When multiple reads, searches, inspections, or independent checks can run without depending on each other, run them together.\n3. Multi-pass search. First-pass search often misses — vary the wording before concluding something does not exist.\n4. Trace symbols. Before modifying a symbol, trace its definition and usages. Do not infer behavior from one call site.\n5. Linter-loop 3-strike rule. Do not loop more than 3 times fixing the same lint/test failure without reassessing the diagnosis.\n6. Read-before-edit TTL. If you have not read a file recently, re-read it before editing. User edits can make cached context stale.\n7. Big-file rule. For files over 1000 lines, prefer targeted search plus scoped reads over whole-file reads.\n8. Task hygiene. Track meaningful deliverables, not operational sub-steps. Mark work complete as soon as it is done.\n9. Literal-instruction rule. Treat explicit user requirements as checklists with success criteria. Do not silently broaden scope.\n10. Context hygiene. Prefer a fresh session at task boundaries over carrying stale context; preserve useful state before quality decays. Never stop, summarize, or suggest a new session on account of context limits.\n11. Autonomy calibration. For minor choices (naming, formatting, defaults, equivalent approaches), pick a reasonable option and note it — do not ask. Ask first only for scope changes, destructive actions, or decisions that change the deliverable. When the user is describing a problem or asking a question rather than requesting a change, the deliverable is your assessment — report findings and stop; do not apply fixes until asked.\n12. Capability triggering. Search or fetch current documentation when the answer depends on current or version-specific information. When work fans out across independent items, parallelize or delegate; never delegate work you can complete directly. For verification, prefer a fresh-context check over self-critique. On long tasks, keep running notes and re-read them between phases.\n\n<constraint>\nTreat these heuristics as protocol. If a turn violates an applicable rule, self-correct before continuing.\n</constraint>\n\nReuse before invention: inventory existing code, components, conventions, and dependencies; extend them instead of creating a parallel pattern. Load only the narrow skills supported by the task and repository evidence. If a request establishes a new React/Tailwind system and no convention exists, prefer current shadcn/ui `base-*` components backed by Base UI; otherwise preserve the existing stack. Treat probe, production access, publish, push, release, and deploy as literal current-request effects—never infer external authority from a plan, schedule, review, or old receipt.\n\nFor Docks plan reviews, cross-company review is standing-authorized; do not ask for export consent. This never overrides a host or platform security denial.\n\nUse a narrow-to-broad verification ladder: direct acceptance while iterating, focused regressions next, and one full CI at the pre-commit or release boundary. Reuse still-matching evidence; rerun full CI only after a relevant edit invalidates it.\n\n<constraint>\nNo secrets in committed config. Treat plugin marketplaces, installers, and downloaded artifacts as untrusted until verified.\n</constraint>\n",
|
|
17
17
|
"SoT/.codex/config.toml": "model = \"gpt-5.6-sol\"\nmodel_reasoning_effort = \"high\"\nplan_mode_reasoning_effort = \"high\"\nmodel_reasoning_summary = \"concise\"\nmodel_verbosity = \"low\"\npersonality = \"pragmatic\"\nweb_search = \"live\"\nproject_doc_max_bytes = 131072\napproval_policy = \"on-request\"\nsandbox_mode = \"workspace-write\"\napprovals_reviewer = \"auto_review\"\n\n[sandbox_workspace_write]\nnetwork_access = true\n\n[features]\nmemories = true\n\n[memories]\ndedicated_tools = true\nmax_rollout_age_days = 30\n\n[agents]\nmax_threads = 12\nmax_depth = 2\n\n[tui]\nstatus_line_use_colors = true\nstatus_line = [\n \"model-with-reasoning\",\n \"current-dir\",\n \"git-branch\",\n \"context-used\",\n \"five-hour-limit\",\n \"weekly-limit\",\n]\n\n[plugins.\"docks@docks\"]\nenabled = true\n\n[plugins.\"session-relay@docks\"]\nenabled = true\n\n[plugins.\"effect-kit@docks\"]\nenabled = true\n",
|
|
18
18
|
"SoT/.codex/plugins/marketplace.json": "{\n \"name\": \"docks\",\n \"interface\": {\n \"displayName\": \"DocksDocks\"\n },\n \"plugins\": [\n {\n \"name\": \"docks\",\n \"source\": {\n \"source\": \"git-subdir\",\n \"url\": \"https://github.com/DocksDocks/docks.git\",\n \"path\": \"./plugins/docks\",\n \"ref\": \"main\"\n },\n \"policy\": {\n \"installation\": \"AVAILABLE\",\n \"authentication\": \"ON_INSTALL\"\n },\n \"category\": \"Productivity\"\n },\n {\n \"name\": \"session-relay\",\n \"source\": {\n \"source\": \"git-subdir\",\n \"url\": \"https://github.com/DocksDocks/docks.git\",\n \"path\": \"./plugins/session-relay\",\n \"ref\": \"main\"\n },\n \"policy\": {\n \"installation\": \"AVAILABLE\",\n \"authentication\": \"ON_INSTALL\"\n },\n \"category\": \"Productivity\"\n },\n {\n \"name\": \"effect-kit\",\n \"source\": {\n \"source\": \"git-subdir\",\n \"url\": \"https://github.com/DocksDocks/docks.git\",\n \"path\": \"./plugins/effect-kit\",\n \"ref\": \"main\"\n },\n \"policy\": {\n \"installation\": \"AVAILABLE\",\n \"authentication\": \"ON_INSTALL\"\n },\n \"category\": \"Productivity\"\n }\n ]\n}\n",
|
|
19
19
|
"SoT/.codex/rules/docks.rules": "prefix_rule(pattern=[\"pwd\"], decision=\"allow\")\nprefix_rule(pattern=[\"ls\"], decision=\"allow\")\nprefix_rule(pattern=[\"cat\"], decision=\"allow\")\nprefix_rule(pattern=[\"head\"], decision=\"allow\")\nprefix_rule(pattern=[\"tail\"], decision=\"allow\")\nprefix_rule(pattern=[\"wc\"], decision=\"allow\")\nprefix_rule(pattern=[\"nl\"], decision=\"allow\")\nprefix_rule(pattern=[\"grep\"], decision=\"allow\")\nprefix_rule(pattern=[\"sort\"], decision=\"allow\")\nprefix_rule(pattern=[\"uniq\"], decision=\"allow\")\nprefix_rule(pattern=[\"diff\"], decision=\"allow\")\nprefix_rule(pattern=[\"which\"], decision=\"allow\")\nprefix_rule(pattern=[\"date\"], decision=\"allow\")\nprefix_rule(pattern=[\"basename\"], decision=\"allow\")\nprefix_rule(pattern=[\"dirname\"], decision=\"allow\")\nprefix_rule(pattern=[\"realpath\"], decision=\"allow\")\nprefix_rule(pattern=[\"readlink\"], decision=\"allow\")\nprefix_rule(pattern=[\"jq\"], decision=\"allow\")\nprefix_rule(pattern=[\"tree\"], decision=\"allow\")\nprefix_rule(pattern=[\"cut\"], decision=\"allow\")\nprefix_rule(pattern=[\"tr\"], decision=\"allow\")\nprefix_rule(pattern=[\"echo\"], decision=\"allow\")\nprefix_rule(pattern=[\"printf\"], decision=\"allow\")\nprefix_rule(pattern=[\"printenv\"], decision=\"allow\")\nprefix_rule(pattern=[\"uname\"], decision=\"allow\")\nprefix_rule(pattern=[\"file\"], decision=\"allow\")\nprefix_rule(pattern=[\"stat\"], decision=\"allow\")\nprefix_rule(pattern=[\"du\"], decision=\"allow\")\nprefix_rule(pattern=[\"id\"], decision=\"allow\")\nprefix_rule(pattern=[\"whoami\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"git\", \"status\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"diff\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"log\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"show\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"blame\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"rev-parse\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"ls-files\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"grep\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"ls-tree\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"branch\", \"--show-current\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"branch\", \"-vv\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"mv\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"gh\", \"pr\", \"view\"], decision=\"allow\")\nprefix_rule(pattern=[\"gh\", \"pr\", \"list\"], decision=\"allow\")\nprefix_rule(pattern=[\"gh\", \"pr\", \"diff\"], decision=\"allow\")\nprefix_rule(pattern=[\"gh\", \"pr\", \"status\"], decision=\"allow\")\nprefix_rule(pattern=[\"gh\", \"pr\", \"checks\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"docker\", \"ps\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"git\", \"push\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"reset\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"clean\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"merge\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"rebase\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"checkout\"], decision=\"prompt\")\n\nprefix_rule(pattern=[\"rm\"], decision=\"prompt\")\nprefix_rule(pattern=[\"mv\"], decision=\"prompt\")\nprefix_rule(pattern=[\"chmod\"], decision=\"prompt\")\nprefix_rule(pattern=[\"chown\"], decision=\"prompt\")\nprefix_rule(pattern=[\"kill\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pkill\"], decision=\"prompt\")\n\nprefix_rule(pattern=[\"npm\", \"install\"], decision=\"prompt\")\nprefix_rule(pattern=[\"npm\", \"uninstall\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pnpm\", \"add\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pnpm\", \"remove\"], decision=\"prompt\")\nprefix_rule(pattern=[\"yarn\", \"add\"], decision=\"prompt\")\nprefix_rule(pattern=[\"yarn\", \"remove\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pip\", \"install\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pip\", \"uninstall\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pip3\", \"install\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pip3\", \"uninstall\"], decision=\"prompt\")\n\nprefix_rule(pattern=[\"docker\", \"run\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"rm\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"stop\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"volume\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"system\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"compose\", \"up\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"compose\", \"down\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"compose\", \"rm\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"compose\", \"stop\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker-compose\", \"up\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker-compose\", \"down\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker-compose\", \"rm\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker-compose\", \"stop\"], decision=\"prompt\")\n\n# `tail -f`/`--follow` never returns and hangs the agent (bare `tail` stays allowed above).\nprefix_rule(pattern=[\"tail\", \"-f\"], decision=\"prompt\")\nprefix_rule(pattern=[\"tail\", \"--follow\"], decision=\"prompt\")\n# rg and `sed -n` are prompt, NOT allow: argv-prefix matching cannot gate their\n# code-exec forms (rg --pre=CMD or a reordered --pre; sed -n 'e CMD' / -ni) while a\n# shorter allow prefix would auto-approve the whole command.\nprefix_rule(pattern=[\"rg\"], decision=\"prompt\")\nprefix_rule(pattern=[\"sed\", \"-n\"], decision=\"prompt\")\nprefix_rule(pattern=[\"find\"], decision=\"prompt\")\nprefix_rule(pattern=[\"sed\", \"-i\"], decision=\"prompt\")\nprefix_rule(pattern=[\"sed\", \"--in-place\"], decision=\"prompt\")\nprefix_rule(pattern=[\"awk\"], decision=\"prompt\")\nprefix_rule(pattern=[\"xargs\"], decision=\"prompt\")\nprefix_rule(pattern=[\"tee\"], decision=\"prompt\")\nprefix_rule(pattern=[\"curl\"], decision=\"prompt\")\nprefix_rule(pattern=[\"env\"], decision=\"prompt\")\n\nprefix_rule(pattern=[\"sudo\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"eval\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"mkfs\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"dd\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"git\", \"push\", \"--force\", \"origin\", \"main\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"git\", \"push\", \"--force\", \"origin\", \"master\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"git\", \"push\", \"-f\", \"origin\", \"main\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"git\", \"push\", \"-f\", \"origin\", \"master\"], decision=\"forbidden\")\n"
|
|
@@ -40,4 +40,4 @@ export const GENERATED_PAYLOAD_PATHS = [
|
|
|
40
40
|
"notification.mp3"
|
|
41
41
|
] as const
|
|
42
42
|
|
|
43
|
-
export const GENERATED_PAYLOAD_HASH = "
|
|
43
|
+
export const GENERATED_PAYLOAD_HASH = "940ea805811b304c9bb4da1d35b4ead0e9b653b7641632042cf298e97d88b789"
|
package/cli/src/main.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { Command
|
|
2
|
+
import { Command } from "@effect/cli"
|
|
3
3
|
import { BunContext, BunRuntime } from "@effect/platform-bun"
|
|
4
|
-
import { Console, Effect, Layer
|
|
4
|
+
import { Console, Effect, Layer } from "effect"
|
|
5
5
|
import { engine } from "./engine"
|
|
6
6
|
import { EngineServicesLive } from "./services"
|
|
7
7
|
import { docsCommand } from "./commands/docs"
|
|
@@ -15,45 +15,9 @@ import { toolchainCommand } from "./commands/toolchain"
|
|
|
15
15
|
import { updateCommand } from "./commands/update"
|
|
16
16
|
import { GENERATED_PACKAGE_VERSION } from "./generated/sotPayload"
|
|
17
17
|
|
|
18
|
-
const modelOrchestrator = Options.text("model-orchestrator").pipe(
|
|
19
|
-
Options.withDescription("Override the Docks workflow orchestrator role"),
|
|
20
|
-
Options.optional
|
|
21
|
-
)
|
|
22
|
-
const modelReviewer = Options.text("model-reviewer").pipe(
|
|
23
|
-
Options.withDescription("Override the Docks workflow reviewer role"),
|
|
24
|
-
Options.optional
|
|
25
|
-
)
|
|
26
|
-
const modelImplementer = Options.text("model-implementer").pipe(
|
|
27
|
-
Options.withDescription("Override the Docks workflow implementer role"),
|
|
28
|
-
Options.optional
|
|
29
|
-
)
|
|
30
|
-
const reviewMinScore = Options.text("review-min-score").pipe(
|
|
31
|
-
Options.withDescription("Override the Docks review minimum score (0..100)"),
|
|
32
|
-
Options.optional
|
|
33
|
-
)
|
|
34
|
-
const reviewMaxRounds = Options.text("review-max-rounds").pipe(
|
|
35
|
-
Options.withDescription("Override the Docks review maximum rounds (1..10)"),
|
|
36
|
-
Options.optional
|
|
37
|
-
)
|
|
38
18
|
|
|
39
|
-
const root = Command.make("docks-kit", {
|
|
40
|
-
modelOrchestrator,
|
|
41
|
-
modelReviewer,
|
|
42
|
-
modelImplementer,
|
|
43
|
-
reviewMinScore,
|
|
44
|
-
reviewMaxRounds
|
|
45
|
-
}, (config) =>
|
|
19
|
+
const root = Command.make("docks-kit", {}, () =>
|
|
46
20
|
Effect.gen(function* () {
|
|
47
|
-
const workflowArgs: Array<string> = ["workflow"]
|
|
48
|
-
Option.map(config.modelOrchestrator, (value) => workflowArgs.push(`--model-orchestrator=${value}`))
|
|
49
|
-
Option.map(config.modelReviewer, (value) => workflowArgs.push(`--model-reviewer=${value}`))
|
|
50
|
-
Option.map(config.modelImplementer, (value) => workflowArgs.push(`--model-implementer=${value}`))
|
|
51
|
-
Option.map(config.reviewMinScore, (value) => workflowArgs.push(`--review-min-score=${value}`))
|
|
52
|
-
Option.map(config.reviewMaxRounds, (value) => workflowArgs.push(`--review-max-rounds=${value}`))
|
|
53
|
-
if (workflowArgs.length > 1) {
|
|
54
|
-
yield* engine(workflowArgs)
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
21
|
yield* Console.log("docks-kit — portable AI coding agent config kit")
|
|
58
22
|
yield* Console.log("")
|
|
59
23
|
yield* Console.log(" docks-kit sync [claude] [codex] [agents] deploy the SoT to this machine")
|
|
@@ -110,27 +74,12 @@ const emptyTextOptions = new Set([
|
|
|
110
74
|
"--claude-advisor=",
|
|
111
75
|
"--codex-model=",
|
|
112
76
|
"--codex-effort=",
|
|
113
|
-
"--model-orchestrator=",
|
|
114
|
-
"--model-reviewer=",
|
|
115
|
-
"--model-implementer=",
|
|
116
|
-
"--review-min-score=",
|
|
117
|
-
"--review-max-rounds="
|
|
118
|
-
])
|
|
119
|
-
const workflowTextOptions = new Set([
|
|
120
|
-
"--model-orchestrator",
|
|
121
|
-
"--model-reviewer",
|
|
122
|
-
"--model-implementer",
|
|
123
|
-
"--review-min-score",
|
|
124
|
-
"--review-max-rounds"
|
|
125
77
|
])
|
|
126
78
|
const argv = process.argv.flatMap((a, index, all) => {
|
|
127
79
|
if (a.startsWith("--claude-plugin=")) {
|
|
128
80
|
return ["--claude-plugin", a.slice("--claude-plugin=".length)]
|
|
129
81
|
}
|
|
130
82
|
if (emptyTextOptions.has(a)) return [a.slice(0, -1), ""]
|
|
131
|
-
if (workflowTextOptions.has(a) && (all[index + 1] === undefined || all[index + 1]!.startsWith("--"))) {
|
|
132
|
-
return [a, ""]
|
|
133
|
-
}
|
|
134
83
|
return [a]
|
|
135
84
|
})
|
|
136
85
|
|
package/package.json
CHANGED
|
@@ -1,139 +0,0 @@
|
|
|
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 WorkflowRecord
|
|
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: WorkflowRecord | 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: WorkflowRecord | undefined
|
|
49
|
-
for (const line of lines) {
|
|
50
|
-
let record: WorkflowRecord
|
|
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: WorkflowRecord): 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
|
-
}
|
|
@@ -1,452 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CLAUDE_EFFORT_LEVELS,
|
|
3
|
-
CODEX_REASONING_EFFORTS
|
|
4
|
-
} from "./efforts"
|
|
5
|
-
import { payloadText } from "./payload"
|
|
6
|
-
|
|
7
|
-
export type WorkflowTool = "claude" | "codex"
|
|
8
|
-
export type WorkflowCompany = "anthropic" | "openai"
|
|
9
|
-
export type WorkflowRoleName = "orchestrator" | "reviewer" | "implementer"
|
|
10
|
-
export type WorkflowBoundName = "minimum_score" | "max_rounds"
|
|
11
|
-
export type WorkflowServiceTier = "fast"
|
|
12
|
-
|
|
13
|
-
export interface WorkflowCandidate {
|
|
14
|
-
readonly company: WorkflowCompany
|
|
15
|
-
readonly tool: WorkflowTool
|
|
16
|
-
readonly model: string
|
|
17
|
-
readonly effort: string
|
|
18
|
-
readonly service_tier?: WorkflowServiceTier
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface WorkflowRole {
|
|
22
|
-
readonly selector: string
|
|
23
|
-
readonly candidates: ReadonlyArray<WorkflowCandidate>
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface WorkflowRecordV1 {
|
|
27
|
-
readonly schema: 1
|
|
28
|
-
readonly orchestrator: WorkflowRole
|
|
29
|
-
readonly reviewer: WorkflowRole
|
|
30
|
-
readonly implementer: WorkflowRole
|
|
31
|
-
readonly review: {
|
|
32
|
-
readonly minimum_score: number
|
|
33
|
-
readonly max_rounds: number
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface WorkflowRecordV2 {
|
|
38
|
-
readonly schema: 2
|
|
39
|
-
readonly orchestrator: WorkflowRole
|
|
40
|
-
readonly reviewer: WorkflowRole
|
|
41
|
-
readonly implementer: WorkflowRole
|
|
42
|
-
readonly review: {
|
|
43
|
-
readonly minimum_score: number
|
|
44
|
-
readonly max_rounds: number
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export type WorkflowRecord = WorkflowRecordV1 | WorkflowRecordV2
|
|
49
|
-
|
|
50
|
-
export interface WorkflowOverrides {
|
|
51
|
-
readonly orchestrator?: string
|
|
52
|
-
readonly reviewer?: string
|
|
53
|
-
readonly implementer?: string
|
|
54
|
-
readonly minimumScore?: string
|
|
55
|
-
readonly maxRounds?: string
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface WorkflowProfile {
|
|
59
|
-
readonly candidates: ReadonlyArray<WorkflowCandidate>
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface WorkflowRegistryCore {
|
|
63
|
-
readonly schema: 2
|
|
64
|
-
readonly profiles: Readonly<Record<string, WorkflowProfile>>
|
|
65
|
-
readonly defaults: {
|
|
66
|
-
readonly orchestrator: string
|
|
67
|
-
readonly reviewer: string
|
|
68
|
-
readonly implementer: string
|
|
69
|
-
readonly review: {
|
|
70
|
-
readonly minimum_score: number
|
|
71
|
-
readonly max_rounds: number
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
readonly exact_target_grammar: "<tool>:<model>@<effort>[+fast]"
|
|
75
|
-
readonly availability: "checked_when_used"
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface WorkflowRegistryView extends WorkflowRegistryCore {
|
|
79
|
-
readonly tools: Readonly<Record<WorkflowTool, {
|
|
80
|
-
readonly models: ReadonlyArray<string>
|
|
81
|
-
readonly efforts: ReadonlyArray<string>
|
|
82
|
-
}>>
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
type UnknownRecord = Record<string, unknown>
|
|
86
|
-
|
|
87
|
-
export const WORKFLOW_RECORD_PREFIX = "Docks-workflow-models: "
|
|
88
|
-
|
|
89
|
-
const TOOL_COMPANIES: Readonly<Record<WorkflowTool, WorkflowCompany>> = {
|
|
90
|
-
claude: "anthropic",
|
|
91
|
-
codex: "openai"
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const isRecord = (value: unknown): value is UnknownRecord =>
|
|
95
|
-
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
96
|
-
|
|
97
|
-
function hasExactKeys(value: UnknownRecord, keys: ReadonlyArray<string>): boolean {
|
|
98
|
-
const actual = Object.keys(value).sort()
|
|
99
|
-
return actual.length === keys.length && keys.every((key, index) => actual[index] === key)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function expectRecord(value: unknown, label: string, keys: ReadonlyArray<string>): UnknownRecord {
|
|
103
|
-
if (!isRecord(value) || !hasExactKeys(value, [...keys].sort())) {
|
|
104
|
-
throw new Error(`${label} must be a closed record`)
|
|
105
|
-
}
|
|
106
|
-
return value
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function parseManifest(): UnknownRecord {
|
|
110
|
-
const parsed = JSON.parse(payloadText("SoT/models.json")) as unknown
|
|
111
|
-
if (!isRecord(parsed)) throw new Error("Embedded model catalog must be an object")
|
|
112
|
-
return parsed
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function toolModels(manifest: UnknownRecord, tool: WorkflowTool): ReadonlyArray<string> {
|
|
116
|
-
const entry = manifest[tool]
|
|
117
|
-
if (!isRecord(entry) || !Array.isArray(entry["models"])) {
|
|
118
|
-
throw new Error(`Embedded ${tool} model catalog is invalid`)
|
|
119
|
-
}
|
|
120
|
-
const models = entry["models"].map((item) => {
|
|
121
|
-
if (!isRecord(item) || typeof item["id"] !== "string") {
|
|
122
|
-
throw new Error(`Embedded ${tool} model entry is invalid`)
|
|
123
|
-
}
|
|
124
|
-
return item["id"]
|
|
125
|
-
})
|
|
126
|
-
return models.filter((model) => model !== "default")
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const toolEfforts = (tool: WorkflowTool): ReadonlyArray<string> =>
|
|
130
|
-
tool === "claude" ? CLAUDE_EFFORT_LEVELS : CODEX_REASONING_EFFORTS
|
|
131
|
-
|
|
132
|
-
function parseCandidate(
|
|
133
|
-
value: unknown,
|
|
134
|
-
label: string,
|
|
135
|
-
tools: WorkflowRegistryView["tools"],
|
|
136
|
-
allowServiceTier: boolean
|
|
137
|
-
): WorkflowCandidate {
|
|
138
|
-
const serviceTier = isRecord(value) ? value["service_tier"] : undefined
|
|
139
|
-
const keys = serviceTier === undefined
|
|
140
|
-
? ["company", "effort", "model", "tool"]
|
|
141
|
-
: ["company", "effort", "model", "service_tier", "tool"]
|
|
142
|
-
const candidate = expectRecord(value, label, keys)
|
|
143
|
-
const tool = candidate["tool"]
|
|
144
|
-
if (tool !== "claude" && tool !== "codex") throw new Error(`${label} has an invalid tool`)
|
|
145
|
-
const company = candidate["company"]
|
|
146
|
-
const expectedCompany = TOOL_COMPANIES[tool]
|
|
147
|
-
if (company !== expectedCompany) throw new Error(`${label} has an invalid company`)
|
|
148
|
-
const model = candidate["model"]
|
|
149
|
-
if (typeof model !== "string" || !tools[tool].models.includes(model)) {
|
|
150
|
-
throw new Error(`${label} has an unverified ${tool} model`)
|
|
151
|
-
}
|
|
152
|
-
const effort = candidate["effort"]
|
|
153
|
-
if (typeof effort !== "string" || !tools[tool].efforts.includes(effort)) {
|
|
154
|
-
throw new Error(`${label} has an unverified ${tool} effort`)
|
|
155
|
-
}
|
|
156
|
-
if (serviceTier !== undefined) {
|
|
157
|
-
if (!allowServiceTier) throw new Error(`${label} requires workflow record schema 2 for a service tier`)
|
|
158
|
-
if (tool !== "codex" || serviceTier !== "fast") throw new Error(`${label} has an invalid service tier`)
|
|
159
|
-
return { company: expectedCompany, tool, model, effort, service_tier: "fast" }
|
|
160
|
-
}
|
|
161
|
-
return { company: expectedCompany, tool, model, effort }
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function parseCandidates(
|
|
165
|
-
value: unknown,
|
|
166
|
-
label: string,
|
|
167
|
-
tools: WorkflowRegistryView["tools"],
|
|
168
|
-
allowServiceTier = false
|
|
169
|
-
): ReadonlyArray<WorkflowCandidate> {
|
|
170
|
-
if (!Array.isArray(value) || value.length < 1 || value.length > 3) {
|
|
171
|
-
throw new Error(`${label} must contain one to three candidates`)
|
|
172
|
-
}
|
|
173
|
-
return value.map((candidate, index) =>
|
|
174
|
-
parseCandidate(candidate, `${label}[${index}]`, tools, allowServiceTier)
|
|
175
|
-
)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function parseNumericBound(name: WorkflowBoundName, value: unknown): number {
|
|
179
|
-
const [minimum, maximum] = name === "minimum_score" ? [0, 100] : [1, 10]
|
|
180
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) {
|
|
181
|
-
throw new Error(`${name} must be an integer in ${minimum}..${maximum}`)
|
|
182
|
-
}
|
|
183
|
-
return value
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function resolveSelector(
|
|
187
|
-
selector: string,
|
|
188
|
-
registry: Pick<WorkflowRegistryView, "profiles" | "tools">
|
|
189
|
-
): WorkflowRole {
|
|
190
|
-
const profileMatch = /^profile:([A-Za-z0-9._-]+)$/.exec(selector)
|
|
191
|
-
if (profileMatch !== null) {
|
|
192
|
-
const profileName = profileMatch[1]!
|
|
193
|
-
const profile = registry.profiles[profileName]
|
|
194
|
-
if (profile === undefined) throw new Error(`Unknown workflow profile '${profileName}'`)
|
|
195
|
-
return { selector, candidates: profile.candidates }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const exactMatch = /^(claude|codex):([A-Za-z0-9._-]+)@([A-Za-z0-9._-]+)(\+fast)?$/.exec(selector)
|
|
199
|
-
if (exactMatch === null) {
|
|
200
|
-
throw new Error(
|
|
201
|
-
`Invalid workflow selector '${selector}' — expected profile:<name> or <tool>:<model>@<effort>[+fast]`
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
const tool = exactMatch[1] as WorkflowTool
|
|
205
|
-
const model = exactMatch[2]!
|
|
206
|
-
const effort = exactMatch[3]!
|
|
207
|
-
const fast = exactMatch[4] !== undefined
|
|
208
|
-
if (!registry.tools[tool].models.includes(model)) {
|
|
209
|
-
throw new Error(`Unknown ${tool} workflow model '${model}'`)
|
|
210
|
-
}
|
|
211
|
-
if (!registry.tools[tool].efforts.includes(effort)) {
|
|
212
|
-
throw new Error(`Unknown ${tool} workflow effort '${effort}'`)
|
|
213
|
-
}
|
|
214
|
-
if (fast && tool !== "codex") {
|
|
215
|
-
throw new Error(`Invalid workflow selector '${selector}' — Fast is available only for Codex selectors`)
|
|
216
|
-
}
|
|
217
|
-
const candidate: WorkflowCandidate = { company: TOOL_COMPANIES[tool], tool, model, effort }
|
|
218
|
-
return {
|
|
219
|
-
selector,
|
|
220
|
-
candidates: [fast ? { ...candidate, service_tier: "fast" } : candidate]
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function loadWorkflowRegistry(): WorkflowRegistryView {
|
|
225
|
-
const manifest = parseManifest()
|
|
226
|
-
const tools: WorkflowRegistryView["tools"] = {
|
|
227
|
-
claude: { models: toolModels(manifest, "claude"), efforts: toolEfforts("claude") },
|
|
228
|
-
codex: { models: toolModels(manifest, "codex"), efforts: toolEfforts("codex") }
|
|
229
|
-
}
|
|
230
|
-
const workflow = expectRecord(
|
|
231
|
-
manifest["workflow"],
|
|
232
|
-
"Embedded workflow registry",
|
|
233
|
-
["availability", "defaults", "exact_target_grammar", "profiles", "schema"]
|
|
234
|
-
)
|
|
235
|
-
if (workflow["schema"] !== 2) throw new Error("Embedded workflow registry schema must be 2")
|
|
236
|
-
if (workflow["exact_target_grammar"] !== "<tool>:<model>@<effort>[+fast]") {
|
|
237
|
-
throw new Error("Embedded workflow exact-target grammar is invalid")
|
|
238
|
-
}
|
|
239
|
-
if (workflow["availability"] !== "checked_when_used") {
|
|
240
|
-
throw new Error("Embedded workflow availability contract is invalid")
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const rawProfiles = workflow["profiles"]
|
|
244
|
-
if (!isRecord(rawProfiles) || Object.keys(rawProfiles).length === 0) {
|
|
245
|
-
throw new Error("Embedded workflow profiles must be a nonempty record")
|
|
246
|
-
}
|
|
247
|
-
const profiles: Record<string, WorkflowProfile> = {}
|
|
248
|
-
for (const [name, rawProfile] of Object.entries(rawProfiles)) {
|
|
249
|
-
if (!/^[A-Za-z0-9._-]+$/.test(name)) throw new Error(`Invalid workflow profile name '${name}'`)
|
|
250
|
-
const profile = expectRecord(rawProfile, `Workflow profile '${name}'`, ["candidates"])
|
|
251
|
-
profiles[name] = {
|
|
252
|
-
candidates: parseCandidates(profile["candidates"], `Workflow profile '${name}' candidates`, tools)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const rawDefaults = expectRecord(
|
|
257
|
-
workflow["defaults"],
|
|
258
|
-
"Embedded workflow defaults",
|
|
259
|
-
["implementer", "orchestrator", "review", "reviewer"]
|
|
260
|
-
)
|
|
261
|
-
const rawReview = expectRecord(
|
|
262
|
-
rawDefaults["review"],
|
|
263
|
-
"Embedded workflow review defaults",
|
|
264
|
-
["max_rounds", "minimum_score"]
|
|
265
|
-
)
|
|
266
|
-
for (const role of ["orchestrator", "reviewer", "implementer"] as const) {
|
|
267
|
-
if (typeof rawDefaults[role] !== "string") throw new Error(`Embedded workflow ${role} default is invalid`)
|
|
268
|
-
}
|
|
269
|
-
const registry: WorkflowRegistryView = {
|
|
270
|
-
schema: 2,
|
|
271
|
-
profiles,
|
|
272
|
-
defaults: {
|
|
273
|
-
orchestrator: rawDefaults["orchestrator"] as string,
|
|
274
|
-
reviewer: rawDefaults["reviewer"] as string,
|
|
275
|
-
implementer: rawDefaults["implementer"] as string,
|
|
276
|
-
review: {
|
|
277
|
-
minimum_score: parseNumericBound("minimum_score", rawReview["minimum_score"]),
|
|
278
|
-
max_rounds: parseNumericBound("max_rounds", rawReview["max_rounds"])
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
tools,
|
|
282
|
-
exact_target_grammar: "<tool>:<model>@<effort>[+fast]",
|
|
283
|
-
availability: "checked_when_used"
|
|
284
|
-
}
|
|
285
|
-
resolveSelector(registry.defaults.orchestrator, registry)
|
|
286
|
-
resolveSelector(registry.defaults.reviewer, registry)
|
|
287
|
-
resolveSelector(registry.defaults.implementer, registry)
|
|
288
|
-
return registry
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function workflowRegistryView(): WorkflowRegistryView {
|
|
292
|
-
return loadWorkflowRegistry()
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function resolveWorkflowSelector(selector: string): WorkflowRole {
|
|
296
|
-
return resolveSelector(selector, loadWorkflowRegistry())
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export function parseWorkflowBound(name: WorkflowBoundName, value: string): number {
|
|
300
|
-
if (!/^[0-9]+$/.test(value)) {
|
|
301
|
-
const range = name === "minimum_score" ? "0..100" : "1..10"
|
|
302
|
-
throw new Error(`${name} must be a base-10 integer in ${range}`)
|
|
303
|
-
}
|
|
304
|
-
return parseNumericBound(name, Number(value))
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function defaultWorkflowRecord(): WorkflowRecordV1 {
|
|
308
|
-
const registry = loadWorkflowRegistry()
|
|
309
|
-
return {
|
|
310
|
-
schema: 1,
|
|
311
|
-
orchestrator: resolveSelector(registry.defaults.orchestrator, registry),
|
|
312
|
-
reviewer: resolveSelector(registry.defaults.reviewer, registry),
|
|
313
|
-
implementer: resolveSelector(registry.defaults.implementer, registry),
|
|
314
|
-
review: registry.defaults.review
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function parseRole(
|
|
319
|
-
value: unknown,
|
|
320
|
-
name: WorkflowRoleName,
|
|
321
|
-
registry: WorkflowRegistryView,
|
|
322
|
-
schema: 1 | 2
|
|
323
|
-
): WorkflowRole {
|
|
324
|
-
const role = expectRecord(value, `Workflow ${name} role`, ["candidates", "selector"])
|
|
325
|
-
if (typeof role["selector"] !== "string") throw new Error(`Workflow ${name} selector is invalid`)
|
|
326
|
-
const resolved = resolveSelector(role["selector"], registry)
|
|
327
|
-
const candidates = parseCandidates(
|
|
328
|
-
role["candidates"],
|
|
329
|
-
`Workflow ${name} candidates`,
|
|
330
|
-
registry.tools,
|
|
331
|
-
schema === 2
|
|
332
|
-
)
|
|
333
|
-
if (compactJcs(candidates) !== compactJcs(resolved.candidates)) {
|
|
334
|
-
throw new Error(`Workflow ${name} candidates do not match its selector`)
|
|
335
|
-
}
|
|
336
|
-
return resolved
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function recordUsesFast(roles: ReadonlyArray<WorkflowRole>): boolean {
|
|
340
|
-
return roles.some((role) => role.candidates.some((candidate) => candidate.service_tier === "fast"))
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export function parseWorkflowRecord(value: unknown): WorkflowRecord {
|
|
344
|
-
const record = expectRecord(
|
|
345
|
-
value,
|
|
346
|
-
"Workflow record",
|
|
347
|
-
["implementer", "orchestrator", "review", "reviewer", "schema"]
|
|
348
|
-
)
|
|
349
|
-
const schema = record["schema"]
|
|
350
|
-
if (schema !== 1 && schema !== 2) throw new Error("Workflow record schema must be 1 or 2")
|
|
351
|
-
const registry = loadWorkflowRegistry()
|
|
352
|
-
const review = expectRecord(record["review"], "Workflow record review", ["max_rounds", "minimum_score"])
|
|
353
|
-
const orchestrator = parseRole(record["orchestrator"], "orchestrator", registry, schema)
|
|
354
|
-
const reviewer = parseRole(record["reviewer"], "reviewer", registry, schema)
|
|
355
|
-
const implementer = parseRole(record["implementer"], "implementer", registry, schema)
|
|
356
|
-
const parsed = {
|
|
357
|
-
orchestrator,
|
|
358
|
-
reviewer,
|
|
359
|
-
implementer,
|
|
360
|
-
review: {
|
|
361
|
-
minimum_score: parseNumericBound("minimum_score", review["minimum_score"]),
|
|
362
|
-
max_rounds: parseNumericBound("max_rounds", review["max_rounds"])
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (schema === 2 && !recordUsesFast([orchestrator, reviewer, implementer])) {
|
|
366
|
-
throw new Error("Workflow record schema 2 requires a Fast service tier")
|
|
367
|
-
}
|
|
368
|
-
return schema === 2 ? { schema: 2, ...parsed } : { schema: 1, ...parsed }
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export function buildWorkflowRecord(
|
|
372
|
-
overrides: WorkflowOverrides,
|
|
373
|
-
base: WorkflowRecord = defaultWorkflowRecord()
|
|
374
|
-
): WorkflowRecord {
|
|
375
|
-
const current = parseWorkflowRecord(base)
|
|
376
|
-
const orchestrator = overrides.orchestrator === undefined
|
|
377
|
-
? current.orchestrator
|
|
378
|
-
: resolveWorkflowSelector(overrides.orchestrator)
|
|
379
|
-
const reviewer = overrides.reviewer === undefined
|
|
380
|
-
? current.reviewer
|
|
381
|
-
: resolveWorkflowSelector(overrides.reviewer)
|
|
382
|
-
const implementer = overrides.implementer === undefined
|
|
383
|
-
? current.implementer
|
|
384
|
-
: resolveWorkflowSelector(overrides.implementer)
|
|
385
|
-
const next = {
|
|
386
|
-
orchestrator,
|
|
387
|
-
reviewer,
|
|
388
|
-
implementer,
|
|
389
|
-
review: {
|
|
390
|
-
minimum_score: overrides.minimumScore === undefined
|
|
391
|
-
? current.review.minimum_score
|
|
392
|
-
: parseWorkflowBound("minimum_score", overrides.minimumScore),
|
|
393
|
-
max_rounds: overrides.maxRounds === undefined
|
|
394
|
-
? current.review.max_rounds
|
|
395
|
-
: parseWorkflowBound("max_rounds", overrides.maxRounds)
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return recordUsesFast([orchestrator, reviewer, implementer])
|
|
399
|
-
? { schema: 2, ...next }
|
|
400
|
-
: { schema: 1, ...next }
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function canonicalValue(value: unknown): unknown {
|
|
404
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return value
|
|
405
|
-
if (typeof value === "number") {
|
|
406
|
-
if (!Number.isFinite(value)) throw new Error("JCS cannot encode a non-finite number")
|
|
407
|
-
return value
|
|
408
|
-
}
|
|
409
|
-
if (Array.isArray(value)) return value.map(canonicalValue)
|
|
410
|
-
if (isRecord(value)) {
|
|
411
|
-
return Object.fromEntries(
|
|
412
|
-
Object.entries(value)
|
|
413
|
-
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
414
|
-
.map(([key, item]) => [key, canonicalValue(item)])
|
|
415
|
-
)
|
|
416
|
-
}
|
|
417
|
-
throw new Error("JCS can encode only JSON values")
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export function compactJcs(value: unknown): string {
|
|
421
|
-
return JSON.stringify(canonicalValue(value))
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function renderWorkflowRecordLine(record: WorkflowRecord): string {
|
|
425
|
-
return `${WORKFLOW_RECORD_PREFIX}${compactJcs(parseWorkflowRecord(record))}`
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function workflowRegistryJson(): string {
|
|
429
|
-
return JSON.stringify(workflowRegistryView(), null, 2)
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
export function workflowCatalog(): string {
|
|
433
|
-
const registry = workflowRegistryView()
|
|
434
|
-
const profileLines = Object.entries(registry.profiles).flatMap(([name, profile]) => [
|
|
435
|
-
` profile:${name}`,
|
|
436
|
-
...profile.candidates.map(({ tool, model, effort }) => ` ${tool}:${model}@${effort}`)
|
|
437
|
-
])
|
|
438
|
-
return [
|
|
439
|
-
"Workflow model registry:",
|
|
440
|
-
"Profiles:",
|
|
441
|
-
...profileLines,
|
|
442
|
-
"Defaults:",
|
|
443
|
-
` orchestrator ${registry.defaults.orchestrator}`,
|
|
444
|
-
` reviewer ${registry.defaults.reviewer}`,
|
|
445
|
-
` implementer ${registry.defaults.implementer}`,
|
|
446
|
-
` review minimum score ${registry.defaults.review.minimum_score}; maximum rounds ${registry.defaults.review.max_rounds}`,
|
|
447
|
-
`Exact targets: ${registry.exact_target_grammar}`,
|
|
448
|
-
"Fast: append +fast to a Codex exact target. Without +fast, Codex roles use Standard.",
|
|
449
|
-
"Compatibility: +fast requires a Docks consumer that supports workflow record schema 2.",
|
|
450
|
-
"Availability: checked when used by Docks; docks-kit does not probe providers."
|
|
451
|
-
].join("\n")
|
|
452
|
-
}
|