Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f7c82c1
release: v0.10.0
sahrizvi Sep 1, 2026
78a87a6
fix(build): stop publishing the orphaned sourcemaps in platform packages
sahrizvi Sep 1, 2026
232c023
fix: close the v0.10.0 release review findings
sahrizvi Sep 1, 2026
eff4c11
fix(skill): delete the duplicate listing renderer, and the bug it was…
sahrizvi Sep 1, 2026
3b3d165
fix: single-source the safety fraction, strip control chars from stde…
sahrizvi Sep 1, 2026
73751bd
fix(ci): drop the invalid `--depth=0` from the tracker-leak job's fetch
sahrizvi Sep 2, 2026
24439e3
fix(review): close the consensus-review findings — 4 major, 7 minor, …
sahrizvi Sep 2, 2026
46edeb5
fix(review): mark the non-verbose fmt escaping for the upstream-share…
sahrizvi Sep 2, 2026
0e3ccf7
fix(review): two regressions the bot reviewers caught in my own revie…
sahrizvi Sep 2, 2026
82d713c
fix(review): close the remaining bot-review findings
sahrizvi Sep 2, 2026
3180c8e
fix(review): second bot round — a vacuous test of mine, and three rea…
sahrizvi Sep 2, 2026
3031d80
fix(review): the examples hint must be copyable, not sanitised
sahrizvi Sep 2, 2026
2160268
fix(review): close the human review — 1 critical, 2 major, 1 minor
sahrizvi Sep 2, 2026
d414568
fix(build): use `!bin/**/*.map` so the exclusion descends
sahrizvi Sep 2, 2026
d3919b6
fix(review): third bot round — a regression of mine, plus a live brea…
sahrizvi Sep 2, 2026
3214a7d
fix(review): pin the call sites, split the file tag set, refuse any e…
sahrizvi Sep 2, 2026
bb3158f
fix(review): require a real tag delimiter, plus two tidy-ups
sahrizvi Sep 2, 2026
9036b4b
fix(review): whitespace terminates a tag — restore attribute-bearing …
sahrizvi Sep 2, 2026
c110f15
fix(review): pagination invariants across pages, plus the remaining r…
sahrizvi Sep 2, 2026
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
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,48 @@ jobs:
- 'test/windows/**'

# ---------------------------------------------------------------------------
# altimate_change start — run the tracker-leak guard on every PR.
# `script/check-tracker-leaks.ts` and its tests already existed but were wired
# into no workflow, so nothing enforced them: v0.10.0 shipped three new
# `AI-####` references into tracked files on this PUBLIC repo before a human
# review caught them. It scans the branch name, the commits ahead of
# origin/main, and the diff, so it needs full history and the base ref.
tracker-leaks:
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
name: Tracker Leaks
runs-on: ubuntu-latest
timeout-minutes: 10
# This job runs pull-request code, so it gets read-only scope and no
# persisted credentials — the checked-out branch must not be able to reach
# the token in `.git/config`. (bot review)
permissions:
contents: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
persist-credentials: false

- uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2
with:
bun-version: "1.3.14"

- name: Fetch base branch
# `--depth=0` is not valid git ("depth 0 is not a positive number") and
# failed the job before the guard could run. `fetch-depth: 0` on the
# checkout above already gives full history, so a plain fetch of the
# base ref is all this needs.
run: git fetch origin main

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT — the base ref is hardcoded here, though the script documents otherwise.

script/check-tracker-leaks.ts says: "Base ref override via --base=<ref> (defaults to origin/main) — CI passes the PR base." CI never passes --base, and fetches main unconditionally.

Moot today: this workflow triggers only on pull_request: branches: [main], so github.base_ref is always main. But it will break silently the day that branch list grows to cover release or hotfix branches, and the failure mode is the guard quietly diffing against the wrong base.

Suggested fix: git fetch origin ${{ github.base_ref || 'main' }} and pass --base=origin/${{ github.base_ref || 'main' }}.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and the script does document --base. Moot while this only triggers on pull_request: branches: [main], but it is a silent break the day a release branch is targeted. Tracking as follow-up.


- name: Check for internal tracker references
# `actions/checkout` lands on the synthetic merge commit in detached
# HEAD, so the script's own `rev-parse --abbrev-ref HEAD` yields "HEAD"
# and the branch-name source -- one of the three it documents -- is
# inert. Pass the real head ref explicitly. (review)
env:
PR_BRANCH: ${{ github.head_ref }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new PR_BRANCH env var has no effect: script/check-tracker-leaks.ts never reads it. The only process.env reference in that script is SKIP_TRACKER_CHECK; branch detection comes from git rev-parse --abbrev-ref HEAD (which yields "HEAD" on the detached merge commit, as the comment notes) or from pushed refs on stdin (empty in CI). So the branch-name scan the comment says this env var enables is still inert in CI. Make the script honor it, e.g. fall back to process.env.PR_BRANCH when rev-parse returns "HEAD", and update the script's documented source list.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 102:

<comment>The new `PR_BRANCH` env var has no effect: `script/check-tracker-leaks.ts` never reads it. The only `process.env` reference in that script is `SKIP_TRACKER_CHECK`; branch detection comes from `git rev-parse --abbrev-ref HEAD` (which yields "HEAD" on the detached merge commit, as the comment notes) or from pushed refs on stdin (empty in CI). So the branch-name scan the comment says this env var enables is still inert in CI. Make the script honor it, e.g. fall back to `process.env.PR_BRANCH` when `rev-parse` returns "HEAD", and update the script's documented source list.</comment>

<file context>
@@ -94,6 +94,12 @@ jobs:
+        # and the branch-name source -- one of the three it documents -- is
+        # inert. Pass the real head ref explicitly. (review)
+        env:
+          PR_BRANCH: ${{ github.head_ref }}
         run: bun script/check-tracker-leaks.ts
   # altimate_change end
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, it was inert. Fixed in 82d713c9a6check-tracker-leaks.ts now reads PR_BRANCH.

run: bun script/check-tracker-leaks.ts
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
# altimate_change end

# Main TypeScript tests — excludes driver E2E tests (separate job) and
# cloud credential tests (local-only).
# ---------------------------------------------------------------------------
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.10.0] - 2026-09-02

Workspaces grow from a memory-only pilot into a working surface: a bound workspace now supplies its custom skills, attaches its own engine, and routes warehouse tools through it. Alongside that, a run no longer dies when one oversized tool result overflows the window, and the ChatGPT-subscription model picker was rebuilt against what the backend actually serves. Everything workspace-related stays behind `ALTIMATE_WORKSPACE=1` and is invisible to anyone not opted in — but the harness-reliability changes are the largest part of this release and apply to **every** session, opted in or not.

### Added

- **A bound workspace's custom skills sync into the project.** Skill bundles attached to the workspace are pulled into `.altimate-code/skill/_workspace/<publicId>/`, where the existing discovery finds them with no other change — a synced skill is listed and invoked exactly like a local one. The tree carries its own `.gitignore` so it never reaches version control, is removed when you opt out or disconnect, and a directory the client did not create is never touched. (#1172)
- **The bound workspace's engine attaches as a derived MCP overlay.** In a bound project the `datamate` MCP entry is derived at config load from the workspace's pinned local engine — never written to disk, overriding IDE, hosted and stale entries. Each turn boundary re-reads the binding, replaces the entry on re-link, and retries a failed handshake once. (#1167)
- **Warehouse tools route through the workspace's engine.** A native warehouse capability is shadowed only when the engine materialised the matching tool and attach attests the engine is its own; the redirect happens after the native safety checks, and fails open with a reason otherwise. `--integrations=local` turns it off. (#1168)
- **An offer to install the engine a bound workspace needs.** A missing engine used to be a toast with a command in it. It is now an offer — Install now / Copy command / Not now — and the install only ever runs from an explicit choice; the next turn boundary picks the installed engine up. (#1169)
- **The model is told what a bound workspace serves.** Redirecting to an engine tool did not make the model choose it first, so every session paid a wasted turn learning the rule. The workspace's capabilities are now stated up front. (#1182)
- **Installs are counted from the shell installers, not just npm.** (#1096)

### Fixed

- **Warehouse SDKs are resolved from disk instead of reported missing.** A bare `import("snowflake-sdk")` inside the compiled binary resolved against bunfs, which has no `node_modules`, so an SDK the user had already installed was invisible and reported as "not installed" — the single root cause behind nine open issues, five of them filed automatically by the telemetry scanner. (#1122)
- **Startup scans prune dependency trees again.** `Glob.Options` lost its `ignore` field in the v1.17.9 bridge, so the two `**/mcp.json` scans filtered results after every directory had already been opened and read. On a repo with `node_modules` installed that cost 12.93 CPU-seconds on every startup. Two intended consequences: a `favicon.*` inside `node_modules`/`dist` is no longer eligible as the project icon, and `altimate-code check` with no file arguments no longer picks up vendored SQL. (#1184)
- **Release binaries embed the live models.dev catalog.** Every platform binary was built with a checked-in test fixture as its bundled catalog, whose newest entry was dated 2026-03-30; verified against the shipped 0.9.7 binary in an isolated `HOME`. (#1188)
- **The ChatGPT-subscription model allowlist matches the backend.** The filter was built from the models.dev catalog rather than what the Codex endpoint serves, so it was wrong in both directions: `gpt-5.2`, `gpt-5.6` and `gpt-5.3-codex` were offered and rejected with HTTP 400, while the current flagship subscription models `gpt-5.6-sol`, `gpt-5.6-luna` and `gpt-5.6-terra` were hidden. Every id was verified against a live Pro credential and the `includes("codex")` substring auto-allow — which cannot express the real policy — was replaced with exact matching. The filter now matches on `api.id` rather than the config map key, so a model aliased in config is no longer deleted. (#1179, closes #1178)
- **`gpt-5.4` and `gpt-5.4-mini` are retired from the picker.** Both retired backend-side on 2026-08-31; the replacements are `gpt-5.6-terra` and `gpt-5.6-luna`. (#1190)
- **A large prompt no longer gets a hard 400 before generating anything.** The per-model output-token reservation never consulted `limit.context`, so on a model where prompt and completion share one window a large system prompt pushed input plus reservation past it. The reservation is now clamped against the context window. (#1196)
- **A run survives a single oversized tool result.** Previously the recovery compaction resent the full conversation, overflowed the same way and terminated with "Session too large to compact". It now summarizes what fits, with tightened context-safety margins and compaction fidelity. (#1171)
- **A credential could survive redaction and be replayed.** The mask that replaces cleared tool output is resent on every later request, and two of its fields bypassed the redactor — so an AWS key, an OpenAI key, a `curl` basic-auth value or a signed URL already in the conversation could still be transmitted after the output it came from was pruned. Both fields now go through the same redactor as the rest of the ledger, which also learned to recognise `curl.exe` and path-qualified `curl`. (#1171)
- **Interactive chat no longer ends answers with a literal `DONE`.** The run-mode completion token was declared on the `builder` agent, which is also the agent behind ordinary conversation, and nothing stripped the token before rendering — so it was appended to final answers in normal chat. It is now scoped to run mode. (#1171)
- **MCP diagnostics say what actually went wrong.** `mcp status` now reports each configured server's real state, including drift between discovered and on-disk config. (#1160) `server unavailable` logged the constant string `"failed"` and discarded `status.error`, the field holding the real message — a `401 Unauthorized`, a transport error, the actual cause. (#1159)
- **The marker check runs in a fresh worktree.** `script/upstream/analyze.ts` imported `minimatch` from the repo root, where it was never declared, so the check failed with `Cannot find package minimatch` before it could run. (#1177)

## [0.9.7] - 2026-08-25

Grep/search reliability fix for everyone, a Codex model-picker unblock, and a first, opt-in look at Workspaces — shared project binding with cloud-synced memory.
Expand Down
5 changes: 4 additions & 1 deletion docs/docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ altimate --agent analyst
| `web` | Start the web UI |
| `agent` | Agent management |
| `auth` | Authentication |
| `mcp` | Model Context Protocol tools |
| `mcp` | Model Context Protocol tools -- `mcp list` to see configured servers, `mcp status` for each server's real connection state and any drift between discovered and on-disk config |
| `acp` | Agent Communication Protocol |
| `models` | List available models |
| `stats` | Usage statistics |
Expand All @@ -54,6 +54,7 @@ altimate --agent analyst
| `--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. |
| `--print-logs` | Print logs to stderr |
| `--log-level <level>` | Set log level: `DEBUG`, `INFO`, `WARN`, `ERROR` |
| `--help`, `-h` | Show help |
Expand Down Expand Up @@ -85,6 +86,8 @@ 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_INTEGRATIONS` | Set to `local` to keep warehouse tools local rather than routing them through a bound workspace's engine |

### Server & Security

Expand Down
16 changes: 16 additions & 0 deletions packages/opencode/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,22 @@ for (const item of targets) {
version: Script.version,
os: [item.os],
cpu: [item.arch],
// altimate_change start — do not publish the orphaned sourcemaps.
// `Bun.build` above runs with `sourcemap: "external"`, so it writes
// `index.js.map` / `worker.js.map` next to the binary — but the bundles
// they describe are compiled INTO the executable, so the package shipped
// `.map` files with no `.js` companion: unusable by any consumer that
// follows `sourceMappingURL`, and not read by the binary at runtime
// (verified — it runs and reports errors normally with them deleted).
// They cost 20MB of a 191MB tarball against npm's ~200MB E413 ceiling.
// Keep emitting them for local debugging of `dist/`; keep them out of
// what we publish.
// `**` because `*` does not descend: a `.map` emitted under a
// `bin/<subdir>/` would still ship. Note the allowlist also means any
// future artifact added OUTSIDE `bin/` is silently dropped from the
// published package. (review)
files: ["bin", "!bin/**/*.map"],
// altimate_change end
},
null,
2,
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 @@ -210,7 +210,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 run without ALTIMATE_WORKSPACE, to manage that entry by hand.`,
`there is not applied. Unlink the project, or restart with ALTIMATE_WORKSPACE unset, to manage that entry by hand.`,
}
}
// altimate_change end
Expand Down Expand Up @@ -373,7 +373,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 run without ALTIMATE_WORKSPACE, first.`,
`here would not connect it. Unlink the project, or restart with ALTIMATE_WORKSPACE unset, first.`,
}
}
}
Expand Down Expand Up @@ -552,7 +552,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 run without ALTIMATE_WORKSPACE, to manage that entry by hand.`,
`or restart with ALTIMATE_WORKSPACE unset, 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 run without ALTIMATE_WORKSPACE, to add it by hand.`,
`whose engine serves that server. Unlink the project, or restart with ALTIMATE_WORKSPACE unset, to add it by hand.`,
)
continue
}
Expand Down
28 changes: 26 additions & 2 deletions packages/opencode/src/altimate/workspace/engine-probes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,36 @@ export async function notify(toast: Toast): Promise<void> {
}
}

// altimate_change start — see `printLine`.
function stripControl(text: string): string {
// C0 minus TAB (a tab is harmless here and legitimate in a name), DEL, and
// C1 (U+0080-U+009F) — U+009B is CSI, so a terminal decoding C1 from UTF-8
// would still act on an escape sequence the C0-only range let through.
// (review)
// eslint-disable-next-line no-control-regex
// U+2028/U+2029 are Unicode line/paragraph separators: not C0 or C1, but they
// still break the one-notice-per-line framing this writer depends on. (bot review)
return text.replace(/[\u0000-\u0008\u000A-\u001F\u007F-\u009F\u2028\u2029]/g, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT — LF is deleted rather than replaced, so a multi-line notice collapses into one run-on line.

The class spans U+000A through U+001F, so printLine("a\nb") emits ab. Suppressing a forged extra line is clearly the intent, but a caller legitimately passing multi-line text loses the break silently. Mapping LF to a space would keep both properties.

(The U+2028/U+2029 extension in 3180c8e43f is a good catch and the right call.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — LF is deleted, so a legitimately multi-line notice collapses. Worth fixing by replacing the separator rather than dropping it; tracking as follow-up.

}
// altimate_change end

/** stderr, deliberately: `run --format json` documents stdout as raw JSON
* events, and this is a status notice, not run output. */
export function printLine(line: string): void {
if (syncInternals.printLine) return syncInternals.printLine(line)
// altimate_change — strip BEFORE the test-seam branch. Stripping after it
// meant the override path (and therefore anything routed through it) never
// got sanitised at all, so the guard covered only one of the two exits.
// (review)
const safe = stripControl(line)
if (syncInternals.printLine) return syncInternals.printLine(safe)
try {
process.stderr.write(line + "\n")
// altimate_change — these lines embed the workspace NAME, which is
// set server-side and never validated for control characters. Writing it
// raw lets a workspace name carrying ANSI escapes repaint or hide
// surrounding output — including, in a CI log, the "engine not usable"
// notice this function exists to deliver. Strip C0 and DEL; the newline is
// added below, so nothing legitimate here needs them. (review)
process.stderr.write(safe + "\n")
} catch {
// A closed stream must not take down the turn.
}
Expand Down
Loading
Loading