Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/docs/configure/learn.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ altimate-code learn promote --publish
|---|---|
| `--yes` | Skip the confirmation prompt. Required outside a terminal. |
| `--allow-flagged` | With `--yes`: approve lessons flagged for mentioning skipping or disabling verification. |
| `--publish` | After promoting, export the approved lessons as a skill and publish it to the linked workspace. Needs `ALTIMATE_WORKSPACE=1`. |
| `--publish` | After promoting, export the approved lessons as a skill and publish it to the linked workspace. Not available when `ALTIMATE_DISABLE_WORKSPACE` is set. |
| `--replace` | With `--publish`: update your own same-name published playbook even if it was published from another checkout. |

### pin / unpin
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/configure/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ altimate-code skill install owner/repo --global # install globally
# Remove
altimate-code skill remove my-tool # remove skill + paired tool

# Publish to the linked workspace (pilot, requires ALTIMATE_WORKSPACE=1)
# Publish to the linked workspace
altimate-code skill publish my-tool # upload every file in the skill directory; re-run to update
```

Expand All @@ -210,7 +210,7 @@ Open the skill browser by typing `/skills` in the prompt (or `<leader>k`):
| Key | Action |
|-----|--------|
| Enter | Use — inserts `/<skill-name>` into the prompt |
| `ctrl+a` | Actions — show, edit, test, remove, or publish the selected skill to the linked workspace (the publish row appears only with `ALTIMATE_WORKSPACE=1`) |
| `ctrl+a` | Actions — show, edit, test, remove, or publish the selected skill to the linked workspace (the publish row is unavailable when `ALTIMATE_DISABLE_WORKSPACE=1` is set) |
| `ctrl+o` | New — scaffold a new skill + CLI tool (`ctrl+n` moves down the list and `ctrl+e` is line-end in the filter box, as in every dialog) |
| `ctrl+g` | Install a skill from a GitHub repo, URL, or local path (`ctrl+i` is Tab in most terminals, so it cannot be the chord) |
| Tab / Shift+Tab | Move between the **Actions · New · Install** buttons in the footer, then Enter — the same three without a chord |
Expand Down
14 changes: 8 additions & 6 deletions docs/docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,20 @@ altimate --agent analyst
| `export` | Export session data |
| `import` | Import session data |
| `session` | Session management |
| `link` | Link this project to an Altimate workspace (pilot, requires `ALTIMATE_WORKSPACE=1`) |
| `workspace` | Show, refresh, sync or unlink this project's workspace from a shell (pilot, requires `ALTIMATE_WORKSPACE=1`) |
| `link` | Link this project to an Altimate workspace |
| `workspace` | Show, refresh, sync or unlink this project's workspace from a shell |
| `trace` | List and view session traces (recordings of agent sessions) |
| `github` | GitHub integration |
| `pr` | Pull request tools |
| `debug` | Troubleshooting tools -- `debug bundle` writes a diagnostic report to send to support; see [Debug Mode](../reference/troubleshooting.md#debug-mode) |
| `upgrade` | Upgrade to latest version |
| `uninstall` | Uninstall altimate |

### Workspaces (pilot)
### Workspaces

Workspace features are off unless `ALTIMATE_WORKSPACE=1` is set. With it:
Workspace features are on by default. When you are signed in to Altimate, altimate-code asks the Altimate service whether the project is linked to a workspace: it sends the project's git `origin` URL (credentials removed), and only if that finds no link, the project's folder path. The answer is kept in memory by the running process and asked again at most every 5 minutes; a failed request is retried on the next turn. Nothing is sent when you are not signed in. Set `ALTIMATE_DISABLE_WORKSPACE=1` to turn all of them off, these lookups included (the value is trimmed and read without regard to case; anything other than `0`, `false` or an empty value counts as set): no post-scan prompt, no `link` or `skill publish`, no `/workspace` menu or sidebar, no skill or memory sync, and no workspace engine. In a project that is already linked, the link is ignored rather than removed: workspace skills already pulled into the project are taken out of service, the agent is not told about the workspace, and the MCP entries the workspace manages can be edited by hand again. Unset it and the link applies again.

With workspaces on:

- `altimate-code link` links the current project to a workspace (or creates one). The sidebar then names the workspace and shows how many memories are not yet synced and when skills last synced.
- From a shell, script or CI job, without the TUI:
Expand All @@ -71,7 +73,7 @@ Workspace features are off unless `ALTIMATE_WORKSPACE=1` is set. With it:
| `--agent <name>` | Start with a specific agent |
| `--yolo` | Auto-approve all permission prompts (explicit `deny` rules still enforced) |
| `--dangerously-skip-permissions` | Same as `--yolo` (alias for upstream compatibility); auto-approves prompts that aren't explicitly denied. `run` subcommand only. |
| `--integrations <local>` | Use only local warehouse tools instead of routing them through a bound workspace's engine (pilot). Sets `ALTIMATE_INTEGRATIONS` for the process, so child processes inherit it. |
| `--integrations <local>` | Use only local warehouse tools instead of routing them through a bound workspace's engine. Sets `ALTIMATE_INTEGRATIONS` for the process, so child processes inherit it. |
| `--print-logs` | Print logs to stderr |
| `--log-level <level>` | Set log level: `DEBUG`, `INFO`, `WARN`, `ERROR` |
| `--help`, `-h` | Show help |
Expand Down Expand Up @@ -104,7 +106,7 @@ Configuration can be controlled via environment variables:
| `ALTIMATE_CLI_DISABLE_TERMINAL_TITLE` | Don't set terminal title |
| `ALTIMATE_CLI_DISABLE_PRUNE` | Disable database pruning |
| `ALTIMATE_CLI_DISABLE_MODELS_FETCH` | Don't fetch models from models.dev |
| `ALTIMATE_WORKSPACE` | Opt into the workspace pilot (`1`). Off by default; nothing about workspaces is active without it |
| `ALTIMATE_DISABLE_WORKSPACE` | Set to `1` to turn off every workspace feature (linking, skill and memory sync, the workspace engine, and the project lookup each session makes). The value is trimmed and read without regard to case; anything other than `0`, `false` or an empty value counts as set. Workspaces are on by default |
| `ALTIMATE_WORKSPACE_WEB_URL` | Development only: the base URL of a local web app's Workspaces pages, used instead of the tenant's for `link`'s browser hand-off and workspace links. Include the mount path, e.g. `http://acme.localhost:3000/workspaces` |
| `ALTIMATE_INTEGRATIONS` | Set to `local` to keep warehouse tools local rather than routing them through a bound workspace's engine |
| `ALTIMATE_CODE_SERVE` | Set to `1` by `altimate-code serve` itself, whether the IDE extension or you launched it. Marks that process as the extension's host — so it is the one that reads the pin variables — and is stripped from every child the bash and shell tools start |
Expand Down
23 changes: 11 additions & 12 deletions packages/core/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,19 @@ export const Flag = {
// altimate_change start — documented ALTIMATE_CLI_ name read first (see `env`)
OPENCODE_WORKSPACE_ID: env("OPENCODE_WORKSPACE_ID"),
// altimate_change end
// Unrelated to ALTIMATE_WORKSPACE (SaaS project-binding pilot) — this gates upstream's multi-instance/worktree control plane.
// Unrelated to ALTIMATE_DISABLE_WORKSPACE (SaaS project-binding kill switch) — this gates upstream's multi-instance/worktree control plane.
OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),

// altimate_change start — pilot flag for the Workspaces feature (post-scan prompt +
// altimate link subcommand). Read as a getter so tests and the runtime `--` middleware
// can flip it between plugin activation and command execution.
//
// Opt-in only — deliberately does NOT inherit ``OPENCODE_EXPERIMENTAL`` (as
// ``enabledByExperimental`` would). The pilot ships behind its own explicit
// gate so users already opted into other experimental features don't get
// this one turned on for them. (Kilo cycle 6.)
// Unrelated to OPENCODE_EXPERIMENTAL_WORKSPACES (multi-instance control plane) — this gates the SaaS project-binding pilot.
get ALTIMATE_WORKSPACE() {
return truthy("ALTIMATE_WORKSPACE")
// altimate_change start — kill switch for the Workspaces feature (project linking, skill and
// memory sync, the workspace engine). Workspaces are on by default; setting this turns every
// part of the feature off and takes already-synced workspace skills out of service. Read as a getter so tests and the
// runtime `--` middleware can flip it between plugin activation and command execution.
// Unrelated to OPENCODE_EXPERIMENTAL_WORKSPACES (multi-instance control plane).
get ALTIMATE_DISABLE_WORKSPACE() {
// A kill switch fails closed: any value but empty, `0` or `false` turns workspaces off, so `yes`
// or `on` cannot leave them running while the operator believes they are off.
const value = env("ALTIMATE_DISABLE_WORKSPACE")?.trim().toLowerCase()
return value !== undefined && value !== "" && value !== "0" && value !== "false"
},
/**
* Workspace precedence escape hatch, set by `--integrations=local`. When on, the
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/altimate/plugin/onboarding-telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as OnboardingTelemetry from "../telemetry/onboarding"
// altimate_change start — AI-8398 workspaces trigger. Reaches into the same
// EventV2 bridge the server/routes/tui.ts uses to publish TuiEvent.CommandExecute
// so the workspace TuiPlugin (packages/opencode/src/plugin/tui/altimate/workspace.tsx)
// runs its post-scan flow. Feature-flagged via Flag.ALTIMATE_WORKSPACE.
// runs its post-scan flow. Off when Flag.ALTIMATE_DISABLE_WORKSPACE is set.
import { Effect } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { AltimateApi } from "@/altimate/api/client"
Expand Down Expand Up @@ -244,7 +244,7 @@ export async function OnboardingTelemetryPlugin(_input: PluginInput): Promise<Ho
// ARM (don't publish yet) — the dialog fires when the session goes idle,
// not the moment project_scan returns. See armWorkspacePromptOnSessionIdle
// above for why the immediate publish raced the LLM's ongoing streaming.
if (Flag.ALTIMATE_WORKSPACE && (await AltimateApi.isConfigured().catch(() => false))) {
if (!Flag.ALTIMATE_DISABLE_WORKSPACE && (await AltimateApi.isConfigured().catch(() => false))) {
void armWorkspacePromptOnSessionIdle(input.sessionID)
}
// altimate_change end
Expand Down
6 changes: 3 additions & 3 deletions packages/opencode/src/altimate/tools/datamate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ async function handleAdd(args: { datamate_id?: string; name?: string; scope?: "p
output:
`This project is linked to workspace "${managed.name}", whose integrations are served by the ` +
`workspace's own engine under the '${DATAMATE_KEY}' MCP server. Adding datamate '${args.datamate_id}' ` +
`there is not applied. Unlink the project, or restart with ALTIMATE_WORKSPACE unset, to manage that entry by hand.`,
`there is not applied. Unlink the project, or restart with ALTIMATE_DISABLE_WORKSPACE=1, to manage that entry by hand.`,
}
}
// altimate_change end
Expand Down Expand Up @@ -493,7 +493,7 @@ async function handleCreate(args: {
output:
`This project is linked to workspace "${managedKey.name}", whose integrations are served by the ` +
`workspace's own engine under the '${DATAMATE_KEY}' MCP server. Creating datamate '${args.name}' ` +
`here would not connect it. Unlink the project, or restart with ALTIMATE_WORKSPACE unset, first.`,
`here would not connect it. Unlink the project, or restart with ALTIMATE_DISABLE_WORKSPACE=1, first.`,
}
}
}
Expand Down Expand Up @@ -672,7 +672,7 @@ async function handleRemove(args: { server_name?: string; scope?: "project" | "g
output:
`This project is linked to workspace "${managedKey.name}", whose integrations are served by the ` +
`workspace's own engine under the '${DATAMATE_KEY}' MCP server. It is not removed. Unlink the project, ` +
`or restart with ALTIMATE_WORKSPACE unset, to manage that entry by hand.`,
`or restart with ALTIMATE_DISABLE_WORKSPACE=1, to manage that entry by hand.`,
}
}
// altimate_change end
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/altimate/tools/mcp-discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const McpDiscoverTool = Tool.define("mcp_discover", {
if (managed) {
lines.push(
`\n'${DATAMATE_KEY}' was not added: this project is linked to workspace "${managed.name}", ` +
`whose engine serves that server. Unlink the project, or restart with ALTIMATE_WORKSPACE unset, to add it by hand.`,
`whose engine serves that server. Unlink the project, or restart with ALTIMATE_DISABLE_WORKSPACE=1, to add it by hand.`,
)
continue
}
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/altimate/workspace/engine-seams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const syncInternals: {
} = {}

export function isEnabled(): boolean {
return CoreFlag.ALTIMATE_WORKSPACE
return !CoreFlag.ALTIMATE_DISABLE_WORKSPACE
}

/** Headless `run`: no TUI can render a toast, so refusals print one stderr
Expand Down
7 changes: 3 additions & 4 deletions packages/opencode/src/altimate/workspace/launch-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ export async function resolveWorkspaceForLaunch(
directory: string,
explicitName: string | undefined,
): Promise<void> {
// Gate on the pilot flag so the flag is invisible to non-opted-in users
// even if they discover it via ``--help``. Silent when opted out — no
// error message that would leak the pilot's existence.
if (!Flag.ALTIMATE_WORKSPACE) return
// Silent under the kill switch: `--workspace` is hidden from ``--help`` then,
// and a user who passes it anyway gets no workspace and no error.
if (Flag.ALTIMATE_DISABLE_WORKSPACE) return
if (explicitName === undefined) return

const binding = await readLocalBinding(directory).catch(() => null)
Expand Down
11 changes: 5 additions & 6 deletions packages/opencode/src/altimate/workspace/memory-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,9 @@ function sessionState(sessionID: string): SessionMemory {
return state
}

/** The mirror rides the workspace pilot flag and honours the memory opt-out.
* Never active for anyone who has not opted into the pilot. */
/** The mirror follows the workspace kill switch and honours the memory opt-out. */
export function isEnabled(): boolean {
return CoreFlag.ALTIMATE_WORKSPACE && !Flag.ALTIMATE_DISABLE_MEMORY
return !CoreFlag.ALTIMATE_DISABLE_WORKSPACE && !Flag.ALTIMATE_DISABLE_MEMORY
}

/** Test seam. Production leaves this unset and resolves the binding from the
Expand Down Expand Up @@ -821,7 +820,7 @@ export async function flushPendingMirrors(timeoutMs = 30_000): Promise<void> {
}

/** Mirror one block. Safe to call unconditionally — returns immediately when
* the pilot flag is off, the project is unbound, or the workspace has memory
* workspaces are disabled, the project is unbound, or the workspace has memory
* disabled. */
export async function mirrorBlock(block: MemoryBlock, directory?: string): Promise<void> {
if (!isEnabled()) return
Expand Down Expand Up @@ -1121,8 +1120,8 @@ function partitionPending(
* Deliberately shares ``partitionPending`` with the sweep rather than re-deriving
* the comparison: this number is a promise about what ``backfill`` would do.
*
* That promise includes the workspace's own memory setting, not just the pilot
* flag. ``backfill`` refuses outright when the bound workspace has memory off,
* That promise includes the workspace's own memory setting, not just the kill
* switch. ``backfill`` refuses outright when the bound workspace has memory off,
* so counting index misses in that state advertises a backlog no action can
* clear — a status line saying "14 not synced" above a sync that answers
* "memory is off for this project". Found end-to-end; both gates have to be the
Expand Down
21 changes: 21 additions & 0 deletions packages/opencode/src/altimate/workspace/pending-turns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// altimate_change - new file
/**
* Background workspace work a turn started without waiting for it. Registered synchronously, at the moment the turn
* starts it, so a one-shot `run` that ends straight away still waits for it on exit: the sync registers itself only
* after a module import and a couple of awaits, and a flush that ran in that window found nothing to wait for.
* Kept free of imports so the turn path can register without loading the sync module.
*/
const KEY = Symbol.for("altimate.workspace.pendingTurns")
const pending: Set<Promise<unknown>> = ((globalThis as Record<symbol, unknown>)[KEY] ??= new Set()) as Set<Promise<unknown>>

export function track(work: Promise<unknown>): void {
pending.add(work)
work.then(
() => pending.delete(work),
() => pending.delete(work),
)
}

export function all(): Promise<unknown>[] {
return [...pending]
}
6 changes: 4 additions & 2 deletions packages/opencode/src/altimate/workspace/skill-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { AltimateApi } from "@/altimate/api/client"
import { credentialDigest, readLocalBinding, resolveBindingOutcome, type CachedBinding } from "./state"
import { altimateRequest, WorkspaceApiError } from "./api-client"
import { readPin, resolveWithinRoot } from "./pin"
import * as PendingTurns from "./pending-turns"

const log = Log.create({ service: "altimate-workspace-skill-sync" })

Expand Down Expand Up @@ -109,7 +110,7 @@ interface RemoteFile {
}

export function isEnabled(): boolean {
return CoreFlag.ALTIMATE_WORKSPACE
return !CoreFlag.ALTIMATE_DISABLE_WORKSPACE
}

function managedRoot(directory: string): string {
Expand Down Expand Up @@ -431,7 +432,8 @@ export async function markRegistryApplied(directory: string): Promise<void> {
* times it was run. The TUI never showed this because it outlives the sync.
* Same reasoning as `awaitBackfill` on the bind path. */
export async function flushPendingSyncs(timeoutMs = 30_000): Promise<void> {
const pending = [...inFlight.values()]
// Turns' background work too: it registers before its sync does (see ./pending-turns).
const pending = [...PendingTurns.all(), ...inFlight.values()]
if (pending.length === 0) return
let timer: ReturnType<typeof setTimeout> | undefined
try {
Expand Down
9 changes: 5 additions & 4 deletions packages/opencode/src/altimate/workspace/welcome-lines.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// altimate_change - new file
//
// The three lines the boot box shows under "What is Altimate Code" when the
// CLI runs in workspace mode: which mode and workspace, which slash commands
// the mode adds, and what the last session got from the workspace. Pure, so
// The three lines the boot box shows under "What is Altimate Code" for a signed-in
// user (workspaces are on by default): which workspace this project is linked to,
// or how to link it; which slash commands the mode adds; and what the last
// session got from the workspace. Pure, so
// the plugin that renders them stays a thin view.
import { describeAge, snapshotCounts, statusHeadline, type AttachSnapshot } from "./attach-snapshot"
import type { BindingOutcome, CachedBinding } from "./state"
Expand Down Expand Up @@ -58,7 +59,7 @@ export function welcomeLines(input: {
if (!binding) {
return {
mode: "Workspace mode · this project is not linked",
commands: "altimate-code link — bind this project to a workspace, then the commands below apply",
commands: "Link it with altimate-code link, or /workspace → Link to a workspace",
integrations: "Integrations: none until the project is linked",
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/opencode/src/cli/cmd/learn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,8 +827,8 @@ const PromoteCommand = effectCmd({
}),
handler: Effect.fn("Cli.learn.promote")(function* (args) {
const name = args.name as string
if (args.publish && !Flag.ALTIMATE_WORKSPACE)
return yield* fail("`--publish` requires the workspace pilot: set ALTIMATE_WORKSPACE=1.")
if (args.publish && Flag.ALTIMATE_DISABLE_WORKSPACE)
return yield* fail("`--publish` needs workspaces, which ALTIMATE_DISABLE_WORKSPACE turns off. Unset it to publish.")
const root = yield* run("", async () => {
Playbook.validateName(name)
return projectRoot()
Expand Down Expand Up @@ -908,7 +908,7 @@ const RollbackCommand = effectCmd({
})
out(
`Restored "${name}" from archived v${restored}.` +
(Flag.ALTIMATE_WORKSPACE ? ` Run \`altimate-code skill publish ${name}\` to share it.` : ""),
(!Flag.ALTIMATE_DISABLE_WORKSPACE ? ` Run \`altimate-code skill publish ${name}\` to share it.` : ""),
)
})
}),
Expand Down
Loading
Loading