docks-kit 0.14.1 → 0.14.2
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 +6 -10
- package/README.md +7 -19
- package/cli/docs/platforms.md +7 -12
- package/cli/docs/sync-layers.md +11 -16
- package/cli/docs/toolchain.md +2 -19
- package/cli/src/commands/status.ts +2 -9
- package/cli/src/commands/toolchain.ts +1 -1
- package/cli/src/engine-native/claudeSync.ts +15 -3
- package/cli/src/engine-native/codexSync.ts +45 -8
- package/cli/src/engine-native/deps.ts +0 -6
- package/cli/src/engine-native/modes.ts +1 -4
- package/cli/src/engine-native/toolchain.ts +0 -2
- package/cli/src/generated/sotPayload.ts +6 -6
- package/package.json +1 -1
- package/cli/src/engine-native/sessionRelayCli.ts +0 -262
- package/cli/src/engine-native/sessionRelayReadiness.ts +0 -94
package/AGENTS.md
CHANGED
|
@@ -12,11 +12,8 @@ Tool-specific instructions live alongside this file:
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
docks-kit runtime and standalone binary support is exactly Linux x64/arm64 and
|
|
15
|
-
macOS x64/arm64.
|
|
16
|
-
|
|
17
|
-
0.16.0; macOS support is aarch64-apple-darwin. Unsupported hosts fail before
|
|
18
|
-
the launcher can fall back to Bun source, and Intel macOS hosts fail closed at
|
|
19
|
-
the Session Relay install boundary.
|
|
15
|
+
macOS x64/arm64. Unsupported hosts fail before the launcher can fall back to
|
|
16
|
+
Bun source.
|
|
20
17
|
|
|
21
18
|
## Repository layout (cross-cutting)
|
|
22
19
|
|
|
@@ -40,11 +37,10 @@ the Session Relay install boundary.
|
|
|
40
37
|
|
|
41
38
|
Codex SoT notes:
|
|
42
39
|
- `SoT/.codex/AGENTS.md` deploys to `~/.codex/AGENTS.md` as global Codex instructions.
|
|
43
|
-
- `SoT/.codex/config.toml` pins Codex to `model = "gpt-5.6-sol"`, sets normal and plan reasoning to `high` with concise summaries, and sets `model_verbosity = "low"`, `personality`, live top-level `web_search`, workspace-write sandboxing with sandboxed command network access, cross-session `memories` (+ dedicated note tools), `[agents]` subagent limits (`max_threads = 12`, `max_depth = 2` — intentionally above Codex defaults for broad parallel kit work; deeper recursion increases cost and predictability risk), a 128 KiB `project_doc_max_bytes` budget for the repo-side AGENTS.md chain (the global `~/.codex/AGENTS.md` is uncapped and not counted), and enables the
|
|
40
|
+
- `SoT/.codex/config.toml` pins Codex to `model = "gpt-5.6-sol"`, sets normal and plan reasoning to `high` with concise summaries, and sets `model_verbosity = "low"`, `personality`, live top-level `web_search`, workspace-write sandboxing with sandboxed command network access, cross-session `memories` (+ dedicated note tools), `[agents]` subagent limits (`max_threads = 12`, `max_depth = 2` — intentionally above Codex defaults for broad parallel kit work; deeper recursion increases cost and predictability risk), a 128 KiB `project_doc_max_bytes` budget for the repo-side AGENTS.md chain (the global `~/.codex/AGENTS.md` is uncapped and not counted), and enables the three Docks plugins `docks@docks`, `plan-lifecycle@docks` (the shared plan lifecycle and an `effect-kit@docks` dependency), and `effect-kit@docks`.
|
|
44
41
|
- `SoT/.codex/rules/*.rules` deploys to `~/.codex/rules/` as kit-managed Codex command policy. This is Codex's equivalent of permission allow/prompt/block rules; user-learned approvals in `~/.codex/rules/default.rules` are preserved.
|
|
45
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.
|
|
46
|
-
-
|
|
47
|
-
- 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 x64/arm64 and macOS 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 three pinned production digests correspond to the `session-relay--v0.16.0` assets and its `SHA256SUMS`; x86_64-apple-darwin is no longer published as of Session Relay 0.16.0.
|
|
43
|
+
- The global prompt SoTs carry the owner's standing authorization for Docks cross-company plan review, which never overrides host or platform denial.
|
|
48
44
|
- 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.
|
|
49
45
|
- Neither global prompt SoT imports `@RTK.md`; Claude uses the hook-backed RTK integration, while Codex has no kit-managed RTK integration.
|
|
50
46
|
- 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.
|
|
@@ -57,9 +53,9 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`), see the matching SoT d
|
|
|
57
53
|
- **Idempotent operations.** Every EngineNative sync step must be safe to re-run. Settings merges, plugin installs, and marketplace adds are all idempotent — re-running with no SoT changes is a no-op.
|
|
58
54
|
- **Removed bash engine.** The bash engine was removed after the `bash-engine-final` tag. `DOCKS_KIT_ENGINE=bash` must fail with the removed-engine message; engine bugs are fixed forward in EngineNative.
|
|
59
55
|
- **Targeted syncs.** `./docks-kit sync` accepts positional targets: `claude`, `codex`, and `agents`. Use the narrowest target that matches the SoT change (for example, `./docks-kit sync codex` for Codex-only config edits); targets can be combined with `--dry-run`, `--skip-rtk`, `--skip-plugin-refresh` (install missing plugins without refreshing existing caches; used by `docks-kit update`), `--reconcile`, `--prune`, `--yes` (auto-accept toolchain prompts), and the deploy-time modifiers `--claude-compact-window=<tokens>` / `--claude-permissive` / `--claude-model=<m>` / `--claude-effort=<level>` / `--claude-advisor=<on|off|default>` / `--codex-model=<m>` / `--codex-effort=<level>` (see `CLAUDE.md` § Deploy-time modifiers).
|
|
60
|
-
- **Additive by default.** Keys present in deployed config but absent from SoT are preserved on default sync. This protects user-only additions, but means drift accumulates — neither flag-less reset can clean it up. The one exception is the Claude `removed` manifest (`claude::_removed_manifest`), a curated list of unambiguous kit-owned artifacts that `claude::sync_removals` force-prunes on every sync
|
|
56
|
+
- **Additive by default.** Keys present in deployed config but absent from SoT are preserved on default sync. This protects user-only additions, but means drift accumulates — neither flag-less reset can clean it up. The one exception is the Claude `removed` manifest (`claude::_removed_manifest`), a curated list of unambiguous kit-owned artifacts that `claude::sync_removals` force-prunes on every sync, including the home-relative `~/.local/bin/session-relay` artifact installed outside `~/.claude`; see `CLAUDE.md` § Pruning stale artifacts.
|
|
61
57
|
- **`--reconcile` / `--prune` are the kit-owned reconcile flags.** Orthogonal — `--reconcile` reconciles the settings layer (SoT-declared keys/tables/arrays win; user-only keys and nested objects are preserved; permissions arrays are replaced wholesale by SoT). `--prune` uninstalls kit-managed installations not in the SoT (plugins, marketplaces, and `~/.agents/skills/*` entries tracked in `~/.agents/.kit-managed-skills`). Combine for a full reset to SoT's kit-managed scope. User-only additions outside the kit's scope (custom env vars, mcpServers, manually-installed skills, third-party plugins not declared in SoT) are always preserved. Each tool's per-tool file documents the specific paths and diff recipes.
|
|
62
|
-
- **SOLID-aligned modules.** `cli/src/engine-native/parseArgs.ts` owns flag parsing/validation. `toolchain.ts` owns the verified-version gate over `SoT/toolchain.json`; `
|
|
58
|
+
- **SOLID-aligned modules.** `cli/src/engine-native/parseArgs.ts` owns flag parsing/validation. `toolchain.ts` owns the verified-version gate over `SoT/toolchain.json`; `bun.ts` owns the shared, memoized Bun bootstrap; `claudeRuntime.ts` owns Claude settings materialization. `claudeSync.ts`, `codexSync.ts`, and `skillsSync.ts` own tool-specific sync logic. `index.ts` is the thin orchestrator. The public CLI seam is `cli/src/engine.ts`.
|
|
63
59
|
- **Small, reviewable changes.** Bundled multi-concern PRs are harder to review and revert. Split an engine/CLI change and a per-tool config change unless the change requires atomicity.
|
|
64
60
|
- **Dry-run before destructive flags.** Always preview with `./docks-kit sync --dry-run` (or the relevant `diff <(jq -S …)` recipe in the per-tool file) before invoking `--reconcile` or `--prune`. User-added permissions / env vars / plugins absent from SoT will be discarded.
|
|
65
61
|
- **SoT prompt files are rules, not explanation.** `SoT/.claude/CLAUDE.md` and `SoT/.codex/AGENTS.md` are loaded into every agent session's prompt context — every line costs prompt tokens on every turn for every user. Restrict their content to rules, heuristics, and `<constraint>` blocks the agent must *act on* during a turn. Do NOT add inline source citations (`Source: …`, attributed quotes), "why this rule exists" preface text, version-watermarking trivia (e.g. "Distilled from X v2.0, captured 2025-11-07"), per-bug workarounds, or installation instructions. Provenance, motivation, and historical context belong in `CLAUDE.md` / `AGENTS.md` at the repo root (humans read once) or in commit messages — never in the SoT. For every line, apply the official test: would removing it cause the agent to make mistakes? If not, cut it — over-instruction degrades adherence on current frontier models.
|
package/README.md
CHANGED
|
@@ -87,18 +87,6 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
|
|
|
87
87
|
versions above the kit-verified pin prompt before installing
|
|
88
88
|
(`--yes` accepts; non-TTY falls back to the pinned verified version
|
|
89
89
|
when possible). `docks-kit toolchain check` shows the full table.
|
|
90
|
-
- **Session Relay CLI** — Claude/Codex syncs ensure the source-pinned
|
|
91
|
-
precompiled `session-relay` command in `~/.local/bin` immediately before
|
|
92
|
-
Session Relay plugin reconciliation. Linux x64/arm64 and macOS arm64 assets
|
|
93
|
-
must match both the committed digest and their release `SHA256SUMS`, then
|
|
94
|
-
pass an exact staged version smoke before atomic replacement; failures
|
|
95
|
-
preserve any prior command. `x86_64-apple-darwin` is no longer published as
|
|
96
|
-
of Session Relay 0.16.0; macOS support is `aarch64-apple-darwin`. The three
|
|
97
|
-
production digests are pinned for the tagged `session-relay--v0.16.0` assets
|
|
98
|
-
and match the same-release `SHA256SUMS`:
|
|
99
|
-
`x86_64-unknown-linux-musl` — `b3ca082dc5ea51e8322be407cdb4bbcaaa05d80bd62c3553f82ab98c1a95498a`
|
|
100
|
-
`aarch64-unknown-linux-musl` — `816b6b8bd2d2c2518ea359a5a21502213347b387a1cc576a0fb9cf541e5646ed`
|
|
101
|
-
`aarch64-apple-darwin` — `da8b114216c3f2301ad582df8e59b49e91953abcc1112b510466b31637fda825`
|
|
102
90
|
- **Model catalog** — `SoT/models.json` is the research-verified source for
|
|
103
91
|
model validation, listings, and pickers.
|
|
104
92
|
- **Claude runtime** — sync materializes three dependency-free Bun `.mjs`
|
|
@@ -126,12 +114,12 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
|
|
|
126
114
|
|
|
127
115
|
## Platform support
|
|
128
116
|
|
|
129
|
-
| Platform | Architecture | docks-kit binary |
|
|
130
|
-
|
|
131
|
-
| Linux | x64 | ✅ | ✅
|
|
132
|
-
| Linux | arm64 | ✅ | ✅
|
|
133
|
-
| macOS | x64 | ✅ | ✅
|
|
134
|
-
| macOS | arm64 | ✅ | ✅
|
|
117
|
+
| Platform | Architecture | docks-kit binary | Sync engine |
|
|
118
|
+
|----------|--------------|------------------|-------------|
|
|
119
|
+
| Linux | x64 | ✅ | ✅ native |
|
|
120
|
+
| Linux | arm64 | ✅ | ✅ native |
|
|
121
|
+
| macOS | x64 | ✅ | ✅ native |
|
|
122
|
+
| macOS | arm64 | ✅ | ✅ native |
|
|
135
123
|
|
|
136
124
|
Details: `docks-kit docs platforms`.
|
|
137
125
|
|
|
@@ -140,7 +128,7 @@ Details: `docks-kit docs platforms`.
|
|
|
140
128
|
Tagging `cli-v*` builds four standalone binaries (Linux x64/arm64 and macOS
|
|
141
129
|
x64/arm64) plus `SHA256SUMS` and attaches them to the GitHub release; npm
|
|
142
130
|
publishes the exact package tarball through trusted publishing with OIDC provenance.
|
|
143
|
-
Package `docks-kit` 0.
|
|
131
|
+
Package `docks-kit` 0.14.2 bundles the CLI + generated payload, so npm releases
|
|
144
132
|
are versioned config snapshots without shipping the authoring `SoT/` tree.
|
|
145
133
|
|
|
146
134
|
## Deeper docs
|
package/cli/docs/platforms.md
CHANGED
|
@@ -2,20 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
The supported host matrix is exact:
|
|
4
4
|
|
|
5
|
-
| Platform | Architecture | docks-kit binary |
|
|
6
|
-
|
|
7
|
-
| Linux | x64 | `docks-kit-linux-x64` |
|
|
8
|
-
| Linux | arm64 | `docks-kit-linux-arm64` |
|
|
9
|
-
| macOS | x64 | `docks-kit-darwin-x64` |
|
|
10
|
-
| macOS | arm64 | `docks-kit-darwin-arm64` |
|
|
11
|
-
|
|
12
|
-
x86_64-apple-darwin is no longer published as of Session Relay 0.16.0; macOS
|
|
13
|
-
support is aarch64-apple-darwin. Intel macOS keeps its docks-kit binary but
|
|
14
|
-
fails closed at the Session Relay install boundary.
|
|
5
|
+
| Platform | Architecture | docks-kit binary | Sync engine |
|
|
6
|
+
|----------|--------------|------------------|-------------|
|
|
7
|
+
| Linux | x64 | `docks-kit-linux-x64` | EngineNative (TS) |
|
|
8
|
+
| Linux | arm64 | `docks-kit-linux-arm64` | EngineNative (TS) |
|
|
9
|
+
| macOS | x64 | `docks-kit-darwin-x64` | EngineNative (TS) |
|
|
10
|
+
| macOS | arm64 | `docks-kit-darwin-arm64` | EngineNative (TS) |
|
|
15
11
|
|
|
16
12
|
Windows and every other host are unsupported. The launcher fails before any
|
|
17
|
-
Bun-from-source fallback
|
|
18
|
-
host without downloading an asset.
|
|
13
|
+
Bun-from-source fallback.
|
|
19
14
|
|
|
20
15
|
EngineNative is the only supported engine on supported hosts.
|
|
21
16
|
`DOCKS_KIT_ENGINE=bash` exits with the removed-engine message and points at the
|
package/cli/docs/sync-layers.md
CHANGED
|
@@ -19,22 +19,19 @@ Order matters — runtime readiness and settings form one transaction:
|
|
|
19
19
|
4. **settings.json merge semantics** — additive: SoT keys win, permissions arrays are
|
|
20
20
|
unioned, user-only keys survive. `--reconcile` replaces permissions arrays
|
|
21
21
|
wholesale instead.
|
|
22
|
-
5. **Removed-artifact pruning** — prune old shell assets, the Stop hook,
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
5. **Removed-artifact pruning** — prune old shell assets, the Stop hook, stale
|
|
23
|
+
kit-owned settings, retired kit-owned plugin enablement, and the stale
|
|
24
|
+
`~/.local/bin/session-relay` command. A flag-less sync removes
|
|
25
|
+
`advisorModel`; an explicit advisor state excludes only that key so the
|
|
26
|
+
modifier owns it.
|
|
25
27
|
6. **Deploy-time modifiers** (`--claude-compact-window`, `--claude-permissive`,
|
|
26
28
|
`--claude-model`, `--claude-effort`, `--claude-advisor`) — deployed file only.
|
|
27
29
|
7. ~/.claude.json (showTurnDuration, user-scoped MCP servers) and connector env
|
|
28
30
|
export.
|
|
29
|
-
8. **
|
|
30
|
-
in `~/.local/bin` for the supported host. A staged asset must match both the
|
|
31
|
-
source digest and same-release `SHA256SUMS`, then report exact version before
|
|
32
|
-
atomic replacement. Failure stops before Session Relay plugin work and
|
|
33
|
-
preserves any prior command.
|
|
34
|
-
9. **Plugins** — seven idempotent passes via the `claude plugin` CLI
|
|
31
|
+
8. **Plugins** — seven idempotent passes via the `claude plugin` CLI
|
|
35
32
|
(marketplaces → install → update → [--prune: uninstall/remove] → re-assert
|
|
36
33
|
SoT enabled-state). Optional opt-ins via `--claude-plugin=<name>`.
|
|
37
|
-
|
|
34
|
+
9. LSP server binaries (npm globals).
|
|
38
35
|
|
|
39
36
|
The statusline reads Claude's native `rate_limits`. There is no OAuth request,
|
|
40
37
|
usage cache, jq/curl runtime dependency, or Stop fetch hook.
|
|
@@ -42,18 +39,16 @@ usage cache, jq/curl runtime dependency, or Stop fetch hook.
|
|
|
42
39
|
## codex (→ ~/.codex, ~/.agents/plugins)
|
|
43
40
|
|
|
44
41
|
bubblewrap check (Linux), config.toml merge (top-level keys replaced
|
|
45
|
-
per-key, [table] blocks replaced wholesale, user-only keys/tables preserved
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
an ensure failure prevents the plugin pass.
|
|
42
|
+
per-key, [table] blocks replaced wholesale, user-only keys/tables preserved,
|
|
43
|
+
and retired kit-owned `[plugins."<id>"]` tables stripped), `--codex-model`
|
|
44
|
+
then `--codex-effort` modifiers, rules, AGENTS.md, personal marketplace file,
|
|
45
|
+
and `codex plugin add` refresh.
|
|
50
46
|
|
|
51
47
|
## agents (→ ~/.agents/skills, ~/.claude/skills symlinks)
|
|
52
48
|
|
|
53
49
|
`npx skills add` per missing manifest slug, Claude symlink healing,
|
|
54
50
|
CLI binaries (agent-browser, effect-solutions — toolchain-gated), and the
|
|
55
51
|
kit-managed snapshot that `--prune` reconciles against.
|
|
56
|
-
This target never ensures or downloads Session Relay.
|
|
57
52
|
|
|
58
53
|
## Reconcile flags
|
|
59
54
|
|
package/cli/docs/toolchain.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
| Field | Meaning |
|
|
6
6
|
|-------|---------|
|
|
7
|
-
| `kind` | `check` (doctor visibility) / `managed` (kit installs + upgrades) / `
|
|
8
|
-
| `policy` | `track` (upgrade toward latest, gated by `verified`) / `present` (install when missing, never upgrade)
|
|
7
|
+
| `kind` | `check` (doctor visibility) / `managed` (kit installs + upgrades) / `pin` (no binary — a version pin for npx-invoked tools, e.g. `skills-cli`) |
|
|
8
|
+
| `policy` | `track` (upgrade toward latest, gated by `verified`) / `present` (install when missing, never upgrade) |
|
|
9
9
|
| `floor` | Minimum acceptable version (below → upgrade automatically) |
|
|
10
10
|
| `verified` | Last kit-tested version — the gate line |
|
|
11
11
|
| `pinnable` | Whether an exact version can be installed (rtk: `RTK_VERSION=vX.Y.Z`) |
|
|
@@ -36,22 +36,6 @@ now kit-approved" act.
|
|
|
36
36
|
toolchain ensure on supported Linux/macOS hosts.
|
|
37
37
|
- **effect-solutions**, **agent-browser** — policy `track`: self-upgrade
|
|
38
38
|
toward npm latest, gated by their `verified` pins.
|
|
39
|
-
- **session-relay** — policy `exact`: Claude/Codex sync and
|
|
40
|
-
`docks-kit toolchain ensure session-relay` select exactly one of three targets
|
|
41
|
-
(`x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`,
|
|
42
|
-
`aarch64-apple-darwin`). `x86_64-apple-darwin` is no longer published as of
|
|
43
|
-
Session Relay 0.16.0; macOS support is `aarch64-apple-darwin`, and Intel
|
|
44
|
-
macOS hosts fail closed before any download. The installer downloads the
|
|
45
|
-
pinned `session-relay--v0.16.0` asset and same-release `SHA256SUMS`, requires
|
|
46
|
-
source pin = selected row = downloaded bytes, chmods/smoke-tests only the
|
|
47
|
-
sibling stage, then renames it over `~/.local/bin/session-relay`. Offline,
|
|
48
|
-
unsupported-host, checksum, chmod, version, download, or rename failures
|
|
49
|
-
preserve an existing command byte-for-byte. The three pinned production
|
|
50
|
-
digests correspond to the tagged `session-relay--v0.16.0` assets and match
|
|
51
|
-
its `SHA256SUMS`:
|
|
52
|
-
`x86_64-unknown-linux-musl` — `b3ca082dc5ea51e8322be407cdb4bbcaaa05d80bd62c3553f82ab98c1a95498a`
|
|
53
|
-
`aarch64-unknown-linux-musl` — `816b6b8bd2d2c2518ea359a5a21502213347b387a1cc576a0fb9cf541e5646ed`
|
|
54
|
-
`aarch64-apple-darwin` — `da8b114216c3f2301ad582df8e59b49e91953abcc1112b510466b31637fda825`
|
|
55
39
|
|
|
56
40
|
jq and curl are `check` rows, not global prerequisites. jq is not consumed by
|
|
57
41
|
normal sync. curl is checked only at a requested Linux/macOS RTK/Bun installer
|
|
@@ -71,6 +55,5 @@ probe falls back to the pinned `verified`, never to an ungated latest.
|
|
|
71
55
|
```
|
|
72
56
|
docks-kit toolchain check # doctor table (also inside docks-kit status)
|
|
73
57
|
docks-kit toolchain ensure rtk # install/upgrade one tool per policy
|
|
74
|
-
docks-kit toolchain ensure session-relay # install exact verified release
|
|
75
58
|
docks-kit sync --yes # unattended: auto-accept gates
|
|
76
59
|
```
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
sotCodexModel
|
|
11
11
|
} from "../manifests"
|
|
12
12
|
import { kitHome } from "../kitHome"
|
|
13
|
-
import { sessionRelayReadiness } from "../engine-native/sessionRelayReadiness"
|
|
14
13
|
|
|
15
14
|
const json = Options.boolean("json").pipe(
|
|
16
15
|
Options.withDescription("Machine-readable output")
|
|
@@ -48,12 +47,11 @@ export const statusCommand = Command.make("status", { json }, (config) =>
|
|
|
48
47
|
const drift = gatherDrift()
|
|
49
48
|
const plugins = pluginsView()
|
|
50
49
|
const skills = skillsView()
|
|
51
|
-
const sessionRelay = sessionRelayReadiness()
|
|
52
50
|
const toolchainTable = yield* engineCapture(["toolchain", "check"])
|
|
53
51
|
|
|
54
52
|
if (config.json) {
|
|
55
53
|
return yield* Console.log(
|
|
56
|
-
JSON.stringify({ kitHome: kitHome(), drift, plugins, skills,
|
|
54
|
+
JSON.stringify({ kitHome: kitHome(), drift, plugins, skills, toolchainTable }, null, 2)
|
|
57
55
|
)
|
|
58
56
|
}
|
|
59
57
|
|
|
@@ -69,11 +67,6 @@ export const statusCommand = Command.make("status", { json }, (config) =>
|
|
|
69
67
|
yield* Console.log(
|
|
70
68
|
`\nPlugins: ${plugins.length} known (${enabled} SoT-enabled) — details: docks-kit plugins list`
|
|
71
69
|
)
|
|
72
|
-
yield* Console.log(
|
|
73
|
-
sessionRelay.state === "ready"
|
|
74
|
-
? `Session Relay: ready for new Codex sessions (v${sessionRelay.version})`
|
|
75
|
-
: `Session Relay: unavailable for new Codex sessions (${sessionRelay.reason})`
|
|
76
|
-
)
|
|
77
70
|
const installed = skills.filter((s) => s.installed).length
|
|
78
71
|
yield* Console.log(
|
|
79
72
|
`Skills: ${skills.length} known (${installed} installed) — details: docks-kit skills list`
|
|
@@ -81,6 +74,6 @@ export const statusCommand = Command.make("status", { json }, (config) =>
|
|
|
81
74
|
})
|
|
82
75
|
).pipe(
|
|
83
76
|
Command.withDescription(
|
|
84
|
-
"Doctor view: deployed-vs-SoT drift, toolchain, plugin/skill counts
|
|
77
|
+
"Doctor view: deployed-vs-SoT drift, toolchain, and plugin/skill counts."
|
|
85
78
|
)
|
|
86
79
|
)
|
|
@@ -2,7 +2,7 @@ import { Args, Command, Options } from "@effect/cli"
|
|
|
2
2
|
import { Effect, Option } from "effect"
|
|
3
3
|
import { bail, engine } from "../engine"
|
|
4
4
|
|
|
5
|
-
const MANAGED = ["rtk", "bun", "effect-solutions", "agent-browser"
|
|
5
|
+
const MANAGED = ["rtk", "bun", "effect-solutions", "agent-browser"]
|
|
6
6
|
|
|
7
7
|
const op = Args.text({ name: "op" }).pipe(
|
|
8
8
|
Args.withDescription("check (default) | ensure <tool>"),
|
|
@@ -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 { ensureSessionRelayCli } from "./sessionRelayCli"
|
|
35
34
|
|
|
36
35
|
export type ClaudeRuntimeState =
|
|
37
36
|
| { readonly kind: "ready"; readonly paths: ClaudeRuntimePaths }
|
|
@@ -81,7 +80,6 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
81
80
|
syncClaudeAdvisor(ctx, ctx.claudeAdvisor)
|
|
82
81
|
syncClaudeJson(ctx)
|
|
83
82
|
syncConnectorEnv(ctx)
|
|
84
|
-
ensureSessionRelayCli(ctx)
|
|
85
83
|
syncPlugins(ctx, claudeDir)
|
|
86
84
|
syncOptionalPlugins(ctx, claudeDir)
|
|
87
85
|
syncLspServers(ctx)
|
|
@@ -449,13 +447,16 @@ const REMOVED_MANIFEST = {
|
|
|
449
447
|
"env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE",
|
|
450
448
|
"env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
|
|
451
449
|
"env.CLAUDE_CODE_FORK_SUBAGENT",
|
|
452
|
-
"env.CLAUDE_CODE_EFFORT_LEVEL"
|
|
450
|
+
"env.CLAUDE_CODE_EFFORT_LEVEL",
|
|
451
|
+
"enabledPlugins.session-relay@docks"
|
|
453
452
|
],
|
|
454
453
|
permissionRules: {
|
|
455
454
|
allow: ["Write(./)"],
|
|
456
455
|
deny: ["Write(**/.env)", "Write(**/.env.local)", "Write(**/secrets/**)"]
|
|
457
456
|
},
|
|
458
457
|
claudeJsonKeys: [] as Array<string>,
|
|
458
|
+
/** Home-relative artifacts the kit installed outside ~/.claude. */
|
|
459
|
+
homeFiles: [".local/bin/session-relay"],
|
|
459
460
|
runtimeReady: {
|
|
460
461
|
hooks: ["notify.sh"],
|
|
461
462
|
files: ["statusline.sh", "fetch-usage.sh"],
|
|
@@ -566,6 +567,17 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
|
|
|
566
567
|
}
|
|
567
568
|
}
|
|
568
569
|
|
|
570
|
+
for (const rel of REMOVED_MANIFEST.homeFiles) {
|
|
571
|
+
const path = p(ctx.home, rel)
|
|
572
|
+
if (!existsSync(path)) continue
|
|
573
|
+
if (ctx.dryRun) {
|
|
574
|
+
echo(`[dry-run] rm ${path}`)
|
|
575
|
+
} else {
|
|
576
|
+
rmSync(path, { force: true })
|
|
577
|
+
filesRemoved++
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
569
581
|
const skeys = pruneJsonKeys(ctx, p(claudeDir, "settings.json"), settingsKeys)
|
|
570
582
|
const permissionRules = prunePermissionRules(
|
|
571
583
|
ctx,
|
|
@@ -10,9 +10,7 @@ import { syncCodexEffort, syncCodexModel, replaceTopLevelSettingInFile } from ".
|
|
|
10
10
|
import { p } from "./exec"
|
|
11
11
|
import type { Ctx } from "./index"
|
|
12
12
|
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
13
|
-
import { sessionRelayReadiness } from "./sessionRelayReadiness"
|
|
14
13
|
import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
|
|
15
|
-
import { ensureSessionRelayCli } from "./sessionRelayCli"
|
|
16
14
|
|
|
17
15
|
export function codexSync(ctx: Ctx): void {
|
|
18
16
|
const codexDir = p(ctx.home, ".codex")
|
|
@@ -28,7 +26,6 @@ export function codexSync(ctx: Ctx): void {
|
|
|
28
26
|
syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
|
|
29
27
|
syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
|
|
30
28
|
removeLegacyDocksMarketplace(ctx, userConfig)
|
|
31
|
-
ensureSessionRelayCli(ctx)
|
|
32
29
|
syncPlugins(ctx, sotConfig)
|
|
33
30
|
}
|
|
34
31
|
|
|
@@ -124,6 +121,7 @@ function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
|
|
|
124
121
|
writeFileSync(staging, before)
|
|
125
122
|
|
|
126
123
|
scrubDeprecatedFeatures(ctx, staging)
|
|
124
|
+
removeRetiredPluginTables(ctx, staging)
|
|
127
125
|
mergeTopLevelSettings(sotConfigText, staging)
|
|
128
126
|
mergeTableSettings(sotConfigText, staging)
|
|
129
127
|
|
|
@@ -184,6 +182,49 @@ function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
|
|
|
184
182
|
change("Codex: scrubbed deprecated [features].use_legacy_landlock")
|
|
185
183
|
}
|
|
186
184
|
|
|
185
|
+
const PLUGIN_TABLE_HEADER = /^\[plugins\."([^"]+)"\][ \t]*$/
|
|
186
|
+
/** Plugin ids the kit retired; their deployed tables are stripped on every sync. */
|
|
187
|
+
const RETIRED_PLUGIN_IDS: Readonly<Record<string, true>> = { "session-relay@docks": true }
|
|
188
|
+
|
|
189
|
+
/** codex::remove_retired_plugin_tables — drop [plugins."<id>"] blocks for retired ids. */
|
|
190
|
+
export function removeRetiredPluginTablesText(content: string): string {
|
|
191
|
+
const lines = content.split("\n")
|
|
192
|
+
if (lines[lines.length - 1] === "") lines.pop()
|
|
193
|
+
let out = ""
|
|
194
|
+
let skipping = false
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
const header = PLUGIN_TABLE_HEADER.exec(line)
|
|
197
|
+
if (header !== null) {
|
|
198
|
+
skipping = RETIRED_PLUGIN_IDS[header[1]!] === true
|
|
199
|
+
if (skipping) continue
|
|
200
|
+
} else if (skipping) {
|
|
201
|
+
if (!line.startsWith("[")) continue
|
|
202
|
+
skipping = false
|
|
203
|
+
}
|
|
204
|
+
out += `${line}\n`
|
|
205
|
+
}
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function retiredPluginTables(content: string): Array<string> {
|
|
210
|
+
return content
|
|
211
|
+
.split("\n")
|
|
212
|
+
.map((line) => PLUGIN_TABLE_HEADER.exec(line)?.[1])
|
|
213
|
+
.filter((id): id is string => id !== undefined && RETIRED_PLUGIN_IDS[id] === true)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function removeRetiredPluginTables(ctx: Ctx, userConfig: string): void {
|
|
217
|
+
const { change } = ctx.services.logger
|
|
218
|
+
if (!existsSync(userConfig)) return
|
|
219
|
+
const content = readFileSync(userConfig, "utf8")
|
|
220
|
+
const present = retiredPluginTables(content)
|
|
221
|
+
if (present.length === 0) return
|
|
222
|
+
|
|
223
|
+
writeFileSync(`${userConfig}.tmp`, removeRetiredPluginTablesText(content))
|
|
224
|
+
renameSync(`${userConfig}.tmp`, userConfig)
|
|
225
|
+
for (const id of present) change(`Codex: removed retired plugin table [plugins."${id}"]`)
|
|
226
|
+
}
|
|
227
|
+
|
|
187
228
|
function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void {
|
|
188
229
|
for (const line of sotConfigText.split("\n")) {
|
|
189
230
|
if (line.startsWith("[")) break
|
|
@@ -407,7 +448,7 @@ export function enabledPluginIdsFromText(configText: string): Array<string> {
|
|
|
407
448
|
if (plugin !== "" && enabled) ids.push(plugin)
|
|
408
449
|
}
|
|
409
450
|
for (const line of configText.split("\n")) {
|
|
410
|
-
const m =
|
|
451
|
+
const m = PLUGIN_TABLE_HEADER.exec(line)
|
|
411
452
|
if (m !== null) {
|
|
412
453
|
flush()
|
|
413
454
|
plugin = m[1]!
|
|
@@ -510,10 +551,6 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
510
551
|
if (refreshed > 0) {
|
|
511
552
|
change(`Codex plugins synced (plugins: ~${refreshed})`)
|
|
512
553
|
ctx.nextStepTriggers.codexRestart = true
|
|
513
|
-
const readiness = sessionRelayReadiness()
|
|
514
|
-
if (readiness.state !== "ready") {
|
|
515
|
-
warn(`Session Relay readiness unavailable after refresh: ${readiness.reason}`)
|
|
516
|
-
}
|
|
517
554
|
}
|
|
518
555
|
if (ctx.skipPluginRefresh && pluginIds.length === 0) verbose("Codex plugins already installed; refresh-only updates skipped")
|
|
519
556
|
if (failed > 0) warn(`${failed} Codex plugin operation(s) failed — re-run sync or install manually`)
|
|
@@ -29,7 +29,6 @@ export type ToolId =
|
|
|
29
29
|
| "bwrap"
|
|
30
30
|
| "agent-browser"
|
|
31
31
|
| "effect-solutions"
|
|
32
|
-
| "session-relay"
|
|
33
32
|
| "chrome-for-testing"
|
|
34
33
|
| "ffplay"
|
|
35
34
|
| "intelephense"
|
|
@@ -241,11 +240,6 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
241
240
|
version: versionProbe("rtk"),
|
|
242
241
|
latest: latestRtk
|
|
243
242
|
}),
|
|
244
|
-
"session-relay": spec("session-relay", "optional", () => "docks-kit toolchain ensure session-relay", {
|
|
245
|
-
resolve: (exec) => pathProbe(p(home(), ".local", "bin", "session-relay"))(exec),
|
|
246
|
-
version: (exec) => exec.capture(p(home(), ".local", "bin", "session-relay"), ["--version"]),
|
|
247
|
-
locate: () => ({ path: p(home(), ".local", "bin", "session-relay"), binDir: p(home(), ".local", "bin") })
|
|
248
|
-
}),
|
|
249
243
|
bun: spec(
|
|
250
244
|
"bun",
|
|
251
245
|
"optional",
|
|
@@ -12,7 +12,6 @@ import type { Ctx } from "./index"
|
|
|
12
12
|
import { isObject, parseJson, type Json } from "./jq"
|
|
13
13
|
import { printModels, validateClaudeModel, validateCodexModel } from "./models"
|
|
14
14
|
import { ensureRtk } from "./claudeSync"
|
|
15
|
-
import { ensureSessionRelayCli } from "./sessionRelayCli"
|
|
16
15
|
import { bunBootstrap } from "./bun"
|
|
17
16
|
import { agentBrowserInstall, effectSolutionsInstall } from "./skillsSync"
|
|
18
17
|
import { ensure, report } from "./toolchain"
|
|
@@ -143,10 +142,8 @@ export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
143
142
|
return ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
144
143
|
case "agent-browser":
|
|
145
144
|
return ensure(ctx, "agent-browser", agentBrowserInstall)
|
|
146
|
-
case "session-relay":
|
|
147
|
-
return ensureSessionRelayCli(ctx)
|
|
148
145
|
default:
|
|
149
|
-
err("toolchain ensure supports managed tools only (rtk, bun, effect-solutions, agent-browser
|
|
146
|
+
err("toolchain ensure supports managed tools only (rtk, bun, effect-solutions, agent-browser)")
|
|
150
147
|
return 2
|
|
151
148
|
}
|
|
152
149
|
}
|
|
@@ -57,8 +57,6 @@ export function installedVersion(ctx: Ctx, tool: ToolId): string {
|
|
|
57
57
|
case "codex":
|
|
58
58
|
case "agent-browser":
|
|
59
59
|
return firstLineField(version(), -1)
|
|
60
|
-
case "session-relay":
|
|
61
|
-
return firstLineField(version(), 1)
|
|
62
60
|
case "git":
|
|
63
61
|
return firstLineField(version(), 2)
|
|
64
62
|
case "node":
|
|
@@ -1,21 +1,21 @@
|
|
|
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.14.
|
|
4
|
+
export const GENERATED_PACKAGE_VERSION = "0.14.2"
|
|
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
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-27\",\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 — the kit SoT default (Opus 5 on the Anthropic API from Claude Code >=2.1.219; Opus 4.6 on Microsoft Foundry)\" },\n { \"id\": \"fable\", \"kind\": \"alias\", \"note\": \"Fable 5 — advisor opt-in 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-5\", \"kind\": \"id\", \"note\": \"Opus 5 — needs Claude Code >=2.1.219\" },\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
|
-
"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) |
|
|
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) | 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). `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.219\", \"note\": \"kit floor — lets the `opus` alias select the newest Opus its provider offers (Anthropic API: 5; Foundry: 4.6) and subsumes Fable 5's >=2.1.170 requirement (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 \"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
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
|
-
"SoT/.claude/settings.json": "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"minimumVersion\": \"2.1.219\",\n \"model\": \"opus\",\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 \"
|
|
12
|
+
"SoT/.claude/settings.json": "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"minimumVersion\": \"2.1.219\",\n \"model\": \"opus\",\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 \"plan-lifecycle@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
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
|
-
"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.\"
|
|
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\": \"
|
|
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.\"plan-lifecycle@docks\"]\nenabled = true\n\n[plugins.\"effect-kit@docks\"]\nenabled = true\n",
|
|
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\": \"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 \"name\": \"plan-lifecycle\",\n \"source\": {\n \"source\": \"git-subdir\",\n \"url\": \"https://github.com/DocksDocks/docks.git\",\n \"path\": \"./plugins/plan-lifecycle\",\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"
|
|
20
20
|
} as const
|
|
21
21
|
|
|
@@ -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 = "9bff028c848a2c0de74c1de7e4197dd146f55f095c88aedeec4a7665ff3564e2"
|
package/package.json
CHANGED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process"
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto"
|
|
3
|
-
import {
|
|
4
|
-
chmodSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
renameSync,
|
|
9
|
-
rmSync,
|
|
10
|
-
statSync
|
|
11
|
-
} from "node:fs"
|
|
12
|
-
import { dirname, join } from "node:path"
|
|
13
|
-
|
|
14
|
-
import { isObject, parseJson, type Json } from "./jq"
|
|
15
|
-
import type { Ctx } from "./index"
|
|
16
|
-
import { ExitError } from "./parseArgs"
|
|
17
|
-
import { payloadText } from "../payload"
|
|
18
|
-
|
|
19
|
-
const REPOSITORY = "DocksDocks/docks"
|
|
20
|
-
const PLUGIN_ID = "session-relay@docks"
|
|
21
|
-
const INSTALL_PATH = "~/.local/bin/session-relay"
|
|
22
|
-
// Intel macOS was retired as of Session Relay 0.16.0: the parent release
|
|
23
|
-
// publishes exactly three native binaries, so darwin/x64 hosts fail closed in
|
|
24
|
-
// sessionRelayTarget before any manifest parse or download.
|
|
25
|
-
const TARGETS = [
|
|
26
|
-
"x86_64-unknown-linux-musl",
|
|
27
|
-
"aarch64-unknown-linux-musl",
|
|
28
|
-
"aarch64-apple-darwin"
|
|
29
|
-
] as const
|
|
30
|
-
|
|
31
|
-
export type SessionRelayTarget = typeof TARGETS[number]
|
|
32
|
-
|
|
33
|
-
export interface SessionRelayManifest {
|
|
34
|
-
readonly kind: "managed-release"
|
|
35
|
-
readonly policy: "exact"
|
|
36
|
-
readonly verified: string
|
|
37
|
-
readonly repository: typeof REPOSITORY
|
|
38
|
-
readonly tag: string
|
|
39
|
-
readonly plugin_id: typeof PLUGIN_ID
|
|
40
|
-
readonly plugin_version: string
|
|
41
|
-
readonly install_path: typeof INSTALL_PATH
|
|
42
|
-
readonly assets: Readonly<Record<SessionRelayTarget, string>>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface SessionRelayInstallOps {
|
|
46
|
-
readonly download: (url: string, destination: string) => boolean
|
|
47
|
-
readonly chmod: (path: string, mode: number) => void
|
|
48
|
-
readonly runVersion: (path: string) => { readonly ok: boolean; readonly stdout: string }
|
|
49
|
-
readonly rename: (from: string, to: string) => void
|
|
50
|
-
readonly uniqueSuffix: () => string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface SessionRelayInstallInput {
|
|
54
|
-
readonly home: string
|
|
55
|
-
readonly dryRun: boolean
|
|
56
|
-
readonly platform: string
|
|
57
|
-
readonly arch: string
|
|
58
|
-
readonly manifestText: string
|
|
59
|
-
readonly log: (line: string) => void
|
|
60
|
-
readonly error?: (line: string) => void
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function fail(input: SessionRelayInstallInput, message: string): never {
|
|
64
|
-
input.error?.(message)
|
|
65
|
-
const error = new ExitError(1)
|
|
66
|
-
error.message = message
|
|
67
|
-
throw error
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function exactKeys(value: { [key: string]: Json }, expected: ReadonlyArray<string>, label: string): void {
|
|
71
|
-
const actual = Object.keys(value).sort()
|
|
72
|
-
const wanted = [...expected].sort()
|
|
73
|
-
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
74
|
-
throw new Error(`${label} violates the closed Session Relay manifest schema`)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function parseSessionRelayManifest(text: string): SessionRelayManifest {
|
|
79
|
-
const value = parseJson(text)
|
|
80
|
-
if (value === undefined || !isObject(value)) throw new Error("Session Relay manifest is not a JSON object")
|
|
81
|
-
exactKeys(
|
|
82
|
-
value,
|
|
83
|
-
["kind", "policy", "verified", "repository", "tag", "plugin_id", "plugin_version", "install_path", "assets"],
|
|
84
|
-
"Session Relay manifest"
|
|
85
|
-
)
|
|
86
|
-
const version = value["verified"]
|
|
87
|
-
if (typeof version !== "string" || !/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(version)) {
|
|
88
|
-
throw new Error("Session Relay manifest verified must be a canonical stable SemVer core")
|
|
89
|
-
}
|
|
90
|
-
const expected = {
|
|
91
|
-
kind: "managed-release",
|
|
92
|
-
policy: "exact",
|
|
93
|
-
verified: version,
|
|
94
|
-
repository: REPOSITORY,
|
|
95
|
-
tag: `session-relay--v${version}`,
|
|
96
|
-
plugin_id: PLUGIN_ID,
|
|
97
|
-
plugin_version: version,
|
|
98
|
-
install_path: INSTALL_PATH
|
|
99
|
-
} as const
|
|
100
|
-
for (const [key, wanted] of Object.entries(expected)) {
|
|
101
|
-
if (value[key] !== wanted) throw new Error(`Session Relay manifest ${key.replaceAll("_", " ")} must be ${wanted}`)
|
|
102
|
-
}
|
|
103
|
-
const assets = value["assets"]
|
|
104
|
-
if (!isObject(assets)) throw new Error("Session Relay manifest assets must be an object")
|
|
105
|
-
exactKeys(assets, TARGETS, "Session Relay manifest assets target set")
|
|
106
|
-
const parsedAssets = {} as Record<SessionRelayTarget, string>
|
|
107
|
-
for (const target of TARGETS) {
|
|
108
|
-
const digest = assets[target]
|
|
109
|
-
if (typeof digest !== "string" || !/^[0-9a-f]{64}$/.test(digest)) {
|
|
110
|
-
throw new Error(`Session Relay manifest digest for ${target} must be 64 lowercase hex characters`)
|
|
111
|
-
}
|
|
112
|
-
parsedAssets[target] = digest
|
|
113
|
-
}
|
|
114
|
-
return { ...expected, assets: parsedAssets }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function manifestEntry(): string {
|
|
118
|
-
const document = parseJson(payloadText("SoT/toolchain.json"))
|
|
119
|
-
const tools = document !== undefined && isObject(document) ? document["tools"] : undefined
|
|
120
|
-
const entry = tools !== undefined && isObject(tools) ? tools["session-relay"] : undefined
|
|
121
|
-
if (entry === undefined) throw new Error("Embedded toolchain manifest has no session-relay entry")
|
|
122
|
-
return JSON.stringify(entry)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function sessionRelayTarget(platform: string, arch: string): SessionRelayTarget {
|
|
126
|
-
if (platform === "linux" && arch === "x64") return "x86_64-unknown-linux-musl"
|
|
127
|
-
if (platform === "linux" && arch === "arm64") return "aarch64-unknown-linux-musl"
|
|
128
|
-
if (platform === "darwin" && arch === "arm64") return "aarch64-apple-darwin"
|
|
129
|
-
throw new Error(`Unsupported host for Session Relay CLI: ${platform}/${arch}; supported: linux x64|arm64, darwin arm64`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function trimOneLineEnding(text: string): string {
|
|
133
|
-
if (text.endsWith("\r\n")) return text.slice(0, -2)
|
|
134
|
-
if (text.endsWith("\n")) return text.slice(0, -1)
|
|
135
|
-
return text
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function exactVersion(ops: SessionRelayInstallOps, path: string, version: string): boolean {
|
|
139
|
-
const result = ops.runVersion(path)
|
|
140
|
-
return result.ok && trimOneLineEnding(result.stdout) === `session-relay ${version}`
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function selectedChecksum(text: string, assetName: string): string {
|
|
144
|
-
const selected = text.split("\n").filter((line) => line.endsWith(` ${assetName}`))
|
|
145
|
-
if (selected.length !== 1 || !new RegExp(`^[0-9a-f]{64} ${assetName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`).test(selected[0]!)) {
|
|
146
|
-
throw new Error(`SHA256SUMS must contain exactly one canonical row for ${assetName}`)
|
|
147
|
-
}
|
|
148
|
-
return selected[0]!.slice(0, 64)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const defaultOps: SessionRelayInstallOps = {
|
|
152
|
-
download: (url, destination) => {
|
|
153
|
-
const result = spawnSync("curl", ["-fL", "--retry", "2", "--connect-timeout", "10", "--output", destination, url], {
|
|
154
|
-
stdio: "inherit"
|
|
155
|
-
})
|
|
156
|
-
return result.error === undefined && result.status === 0
|
|
157
|
-
},
|
|
158
|
-
chmod: chmodSync,
|
|
159
|
-
runVersion: (path) => {
|
|
160
|
-
const result = spawnSync(path, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
161
|
-
return { ok: result.error === undefined && result.status === 0, stdout: result.stdout ?? "" }
|
|
162
|
-
},
|
|
163
|
-
rename: renameSync,
|
|
164
|
-
uniqueSuffix: () => `${process.pid}-${randomBytes(8).toString("hex")}`
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function installSessionRelayCli(
|
|
168
|
-
input: SessionRelayInstallInput,
|
|
169
|
-
ops: SessionRelayInstallOps = defaultOps
|
|
170
|
-
): void {
|
|
171
|
-
let manifest: SessionRelayManifest
|
|
172
|
-
let target: SessionRelayTarget
|
|
173
|
-
try {
|
|
174
|
-
manifest = parseSessionRelayManifest(input.manifestText)
|
|
175
|
-
target = sessionRelayTarget(input.platform, input.arch)
|
|
176
|
-
} catch (error) {
|
|
177
|
-
fail(input, error instanceof Error ? error.message : String(error))
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (input.dryRun) {
|
|
181
|
-
input.log(
|
|
182
|
-
`[dry-run] ensure Session Relay CLI ${manifest.verified} from ${manifest.repository}@${manifest.tag} (${target}) -> ${manifest.install_path}`
|
|
183
|
-
)
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const stable = join(input.home, ".local", "bin", "session-relay")
|
|
188
|
-
if (existsSync(stable) && exactVersion(ops, stable, manifest.verified)) return
|
|
189
|
-
|
|
190
|
-
const parent = dirname(stable)
|
|
191
|
-
try {
|
|
192
|
-
mkdirSync(parent, { recursive: true, mode: 0o755 })
|
|
193
|
-
if (!statSync(parent).isDirectory()) {
|
|
194
|
-
fail(input, `Session Relay install parent is not a directory: ${parent}`)
|
|
195
|
-
}
|
|
196
|
-
} catch (error) {
|
|
197
|
-
if (error instanceof ExitError) throw error
|
|
198
|
-
fail(input, `Cannot prepare Session Relay install directory ${parent}: ${error instanceof Error ? error.message : String(error)}`)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const suffix = ops.uniqueSuffix()
|
|
202
|
-
const stage = join(parent, `.session-relay.stage-${suffix}`)
|
|
203
|
-
const checksumFile = join(parent, `.session-relay.checksums-${suffix}`)
|
|
204
|
-
const assetName = `session-relay-${target}`
|
|
205
|
-
const baseUrl = `https://github.com/${manifest.repository}/releases/download/${manifest.tag}`
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
if (!ops.download(`${baseUrl}/${assetName}`, stage)) fail(input, `Failed to download pinned Session Relay asset ${assetName}`)
|
|
209
|
-
if (!ops.download(`${baseUrl}/SHA256SUMS`, checksumFile)) fail(input, "Failed to download pinned Session Relay SHA256SUMS")
|
|
210
|
-
|
|
211
|
-
let checksumDigest: string
|
|
212
|
-
try {
|
|
213
|
-
checksumDigest = selectedChecksum(readFileSync(checksumFile, "utf8"), assetName)
|
|
214
|
-
} catch (error) {
|
|
215
|
-
fail(input, error instanceof Error ? error.message : String(error))
|
|
216
|
-
}
|
|
217
|
-
const sourceDigest = manifest.assets[target]
|
|
218
|
-
if (checksumDigest !== sourceDigest) fail(input, `Session Relay source pin does not match SHA256SUMS for ${assetName}`)
|
|
219
|
-
const downloadedDigest = createHash("sha256").update(readFileSync(stage)).digest("hex")
|
|
220
|
-
if (downloadedDigest !== sourceDigest) fail(input, `Downloaded Session Relay checksum mismatch for ${assetName}`)
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
ops.chmod(stage, 0o755)
|
|
224
|
-
} catch (error) {
|
|
225
|
-
fail(input, `Failed to chmod staged Session Relay CLI: ${error instanceof Error ? error.message : String(error)}`)
|
|
226
|
-
}
|
|
227
|
-
if (!exactVersion(ops, stage, manifest.verified)) {
|
|
228
|
-
fail(input, `Staged Session Relay CLI did not report exact version session-relay ${manifest.verified}`)
|
|
229
|
-
}
|
|
230
|
-
try {
|
|
231
|
-
ops.rename(stage, stable)
|
|
232
|
-
} catch (error) {
|
|
233
|
-
fail(input, `Failed to atomically replace Session Relay CLI: ${error instanceof Error ? error.message : String(error)}`)
|
|
234
|
-
}
|
|
235
|
-
input.log(`Session Relay CLI ready (${manifest.verified})`)
|
|
236
|
-
} finally {
|
|
237
|
-
try {
|
|
238
|
-
rmSync(stage, { force: true })
|
|
239
|
-
} catch {
|
|
240
|
-
// A cleanup failure must not turn a successful atomic replacement into
|
|
241
|
-
// an install failure or alter a pre-existing stable executable.
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
rmSync(checksumFile, { force: true })
|
|
245
|
-
} catch {
|
|
246
|
-
// Same failure-preservation rule as the staged executable above.
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export function ensureSessionRelayCli(ctx: Ctx): number {
|
|
252
|
-
installSessionRelayCli({
|
|
253
|
-
home: ctx.home,
|
|
254
|
-
dryRun: ctx.dryRun,
|
|
255
|
-
platform: ctx.services.platform.raw(),
|
|
256
|
-
arch: process.arch,
|
|
257
|
-
manifestText: manifestEntry(),
|
|
258
|
-
log: ctx.dryRun ? ctx.services.logger.echo : ctx.services.logger.change,
|
|
259
|
-
error: ctx.services.logger.err
|
|
260
|
-
})
|
|
261
|
-
return 0
|
|
262
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process"
|
|
2
|
-
|
|
3
|
-
import { isObject, parseJson } from "./jq"
|
|
4
|
-
|
|
5
|
-
const SESSION_RELAY_PLUGIN_ID = "session-relay@docks"
|
|
6
|
-
|
|
7
|
-
export type SessionRelayReadinessReason =
|
|
8
|
-
| "codex_cli_unavailable"
|
|
9
|
-
| "plugin_list_failed"
|
|
10
|
-
| "invalid_plugin_list"
|
|
11
|
-
| "plugin_missing"
|
|
12
|
-
| "plugin_ambiguous"
|
|
13
|
-
| "plugin_not_installed"
|
|
14
|
-
| "plugin_disabled"
|
|
15
|
-
|
|
16
|
-
export interface SessionRelayReadiness {
|
|
17
|
-
readonly schema: 1
|
|
18
|
-
readonly state: "ready" | "unavailable"
|
|
19
|
-
readonly reason: SessionRelayReadinessReason | null
|
|
20
|
-
readonly version: string | null
|
|
21
|
-
readonly installed: boolean
|
|
22
|
-
readonly enabled: boolean
|
|
23
|
-
readonly scope: "new_sessions"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface CodexPluginListProbe {
|
|
27
|
-
readonly status: number | null
|
|
28
|
-
readonly stdout: string
|
|
29
|
-
readonly errorCode: string | null
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function unavailable(
|
|
33
|
-
reason: SessionRelayReadinessReason,
|
|
34
|
-
fields: { version?: string | null; installed?: boolean; enabled?: boolean } = {}
|
|
35
|
-
): SessionRelayReadiness {
|
|
36
|
-
return {
|
|
37
|
-
schema: 1,
|
|
38
|
-
state: "unavailable",
|
|
39
|
-
reason,
|
|
40
|
-
version: fields.version ?? null,
|
|
41
|
-
installed: fields.installed ?? false,
|
|
42
|
-
enabled: fields.enabled ?? false,
|
|
43
|
-
scope: "new_sessions"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function classifySessionRelayReadiness(probe: CodexPluginListProbe): SessionRelayReadiness {
|
|
48
|
-
if (probe.errorCode === "ENOENT") return unavailable("codex_cli_unavailable")
|
|
49
|
-
if (probe.errorCode !== null || probe.status !== 0) return unavailable("plugin_list_failed")
|
|
50
|
-
|
|
51
|
-
const value = parseJson(probe.stdout)
|
|
52
|
-
if (value === undefined || !isObject(value) || !Array.isArray(value["installed"])) {
|
|
53
|
-
return unavailable("invalid_plugin_list")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const matches = value["installed"].filter(
|
|
57
|
-
(entry) => isObject(entry) && entry["pluginId"] === SESSION_RELAY_PLUGIN_ID
|
|
58
|
-
)
|
|
59
|
-
if (matches.length === 0) return unavailable("plugin_missing")
|
|
60
|
-
if (matches.length !== 1) return unavailable("plugin_ambiguous")
|
|
61
|
-
|
|
62
|
-
const row = matches[0]
|
|
63
|
-
if (row === undefined || !isObject(row)) return unavailable("invalid_plugin_list")
|
|
64
|
-
const version = row["version"]
|
|
65
|
-
const installed = row["installed"]
|
|
66
|
-
const enabled = row["enabled"]
|
|
67
|
-
if (typeof version !== "string" || version.length === 0 || typeof installed !== "boolean" || typeof enabled !== "boolean") {
|
|
68
|
-
return unavailable("invalid_plugin_list")
|
|
69
|
-
}
|
|
70
|
-
if (!installed) return unavailable("plugin_not_installed", { version, enabled })
|
|
71
|
-
if (!enabled) return unavailable("plugin_disabled", { version, installed })
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
schema: 1,
|
|
75
|
-
state: "ready",
|
|
76
|
-
reason: null,
|
|
77
|
-
version,
|
|
78
|
-
installed,
|
|
79
|
-
enabled,
|
|
80
|
-
scope: "new_sessions"
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function sessionRelayReadiness(): SessionRelayReadiness {
|
|
85
|
-
const result = spawnSync("codex", ["plugin", "list", "--json"], {
|
|
86
|
-
encoding: "utf8",
|
|
87
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
88
|
-
})
|
|
89
|
-
return classifySessionRelayReadiness({
|
|
90
|
-
status: result.status,
|
|
91
|
-
stdout: result.stdout ?? "",
|
|
92
|
-
errorCode: (result.error as NodeJS.ErrnoException | undefined)?.code ?? null
|
|
93
|
-
})
|
|
94
|
-
}
|