Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
81da05f
docs: spec for save_output expert-authored output files (#14)
lowlorenz Jul 2, 2026
1832c6f
docs: spec for provider-agnostic image downscaling on expert upload
lowlorenz Jul 6, 2026
92e0861
docs: spec for expert LLM call retry + timeout
lowlorenz Jul 6, 2026
609589e
docs: implementation plan for image downscale + expert retry
lowlorenz Jul 6, 2026
b53e7a1
feat: downscaleToLimit helper caps image long edge with sharp
lowlorenz Jul 6, 2026
061582b
feat: cap expert image uploads at CHRONOS_MAX_IMAGE_DIMENSION (defaul…
lowlorenz Jul 6, 2026
4baa9d9
feat: retry policy for expert LLM calls (backoff, permanent-error cla…
lowlorenz Jul 6, 2026
cf6c29e
feat: expert LLM calls retry transient failures with 300s per-attempt…
lowlorenz Jul 6, 2026
03d716e
feat: settings for image cap, expert retries, and request timeout
lowlorenz Jul 6, 2026
e9b5c35
docs: spec for task subagent arbitrary image / plain-task modes
lowlorenz Jul 16, 2026
2590722
docs: implementation plan for task image / plain-task modes
lowlorenz Jul 16, 2026
f7de725
wip: in-progress archive-support / collection-context work (checkpoint)
lowlorenz Jul 16, 2026
1dc0bf1
feat: loadImageAsPng normalizes any image to a downscaled PNG
lowlorenz Jul 16, 2026
e4eee17
feat: runExpertTurn supports optional source and arbitrary image
lowlorenz Jul 16, 2026
45a3bf4
feat: task tool accepts arbitrary image and optional source
lowlorenz Jul 16, 2026
d851c2b
feat: task_batch runs over page_ids or arbitrary images, source optional
lowlorenz Jul 16, 2026
178960d
feat: batch chips show item label (page or image) in the webview
lowlorenz Jul 16, 2026
b197636
fix: follow-up source inheritance + guarded output-file paths + doc a…
lowlorenz Jul 16, 2026
e8f682f
Merge branch 'dev' into review/archive-support-integration
lowlorenz Jul 28, 2026
402ae8d
Merge branch 'dev' into fix/archive-support-blockers
lowlorenz Jul 28, 2026
6c2b1fb
docs: spec + plan for the archive-support blocker fixes
lowlorenz Jul 28, 2026
63fd83b
docs: patch the blocker plan before execution
lowlorenz Jul 28, 2026
7e2a373
fix: strip conflict marker from the walkthrough, fix seeded typo
lowlorenz Jul 28, 2026
58c3c82
fix: enforce the expert timeout by aborting, not pi-ai timeoutMs
lowlorenz Jul 28, 2026
9e29277
fix: resolve review findings on the expert timeout-as-abort retry
lowlorenz Jul 28, 2026
12f489a
chore: ignore .vscode-test at any depth, not just chronos-vscode/
lowlorenz Jul 28, 2026
99f993d
fix: follow-up output_file follows the inherited source
lowlorenz Jul 28, 2026
cd31173
fix: details.source follows the inherited source too (R1)
lowlorenz Jul 28, 2026
29e5ac6
fix: cross-platform canary expectations + shared source precedence (R…
lowlorenz Jul 28, 2026
fb12612
fix: error on an ambiguous bare source basename
lowlorenz Jul 28, 2026
032b5e9
fix: change_source additions survive session_start
lowlorenz Jul 28, 2026
f18ca4b
fix: replay change_source additions across /select-collection too (R1…
lowlorenz Jul 28, 2026
0c41206
docs: task 7 must watch its UI-test assertion fail first
lowlorenz Jul 28, 2026
9474a4e
fix: move replayExtraMembers out of the pi entrypoint; harden R5 cana…
lowlorenz Jul 28, 2026
dbb46e1
fix: identify collections by filename, not display name
lowlorenz Jul 28, 2026
76dac02
fix: key collection data/memory paths by id, not display name (R14, R18)
lowlorenz Jul 28, 2026
aa6a775
fix: nested sources resolve to the agent's data dir
lowlorenz Jul 28, 2026
0d570d4
fix: derive nested-source data key on cold cache, not basename (F1/F2…
lowlorenz Jul 28, 2026
6e4badc
fix: normalize the nested-source ref on both sides of the RPC boundar…
lowlorenz Jul 29, 2026
238e0f0
fix: fork carries the forked session's collection/extraMembers forwar…
lowlorenz Jul 29, 2026
e8ee491
fix: /select-source's exact ref match must win over an ambiguous base…
lowlorenz Jul 29, 2026
f4835d1
fix: effectiveRef must not treat source: "" as no source at all (Item A)
lowlorenz Jul 29, 2026
4ee84f6
fix: delete the collection display-name migration/fallback (Item B)
lowlorenz Jul 29, 2026
b32c36f
test: npm test builds the agent it derives expectations from (F3, F4,…
lowlorenz Jul 29, 2026
c76cfeb
Merge remote-tracking branch 'origin/dev' into fix/archive-support-bl…
lowlorenz Jul 29, 2026
af898fa
fix: never bind a source silently to the wrong document
lowlorenz Jul 29, 2026
be0623a
chore: stop tracking implementation plans
lowlorenz Jul 29, 2026
d4ac767
ci: install the agent package the UI test builds, and run its canaries
lowlorenz Jul 29, 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
29 changes: 25 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,22 @@ jobs:
chronos/package-lock.json
chronos-vscode/package-lock.json

# chronos pi-package — npm ci resolves the @mariozechner peer deps; tsc emits dist/
# chronos pi-package — npm ci resolves the pi peer deps; tsc emits dist/
- name: Install chronos
working-directory: chronos
run: npm ci
- name: Build chronos (tsc)
working-directory: chronos
run: npm run build

# The agent's canaries (image downscaling, expert retry, timeout-as-abort,
# collection context / refs / data keys / session sidecars). They import the
# BUILD OUTPUT, so they run after the build above — `npm test` rebuilds anyway.
# Nothing in CI exercised these before, so ~140 assertions were local-only.
- name: Agent canaries
working-directory: chronos
run: npm test

# chronos-vscode — esbuild does NOT type-check, so run both tsconfigs explicitly
- name: Install chronos-vscode
working-directory: chronos-vscode
Expand Down Expand Up @@ -64,7 +72,19 @@ jobs:
with:
node-version: "24"
cache: npm
cache-dependency-path: chronos-vscode/package-lock.json
cache-dependency-path: |
chronos/package-lock.json
chronos-vscode/package-lock.json

# The agent package's OWN devDeps (typescript, @types/node) are required here,
# not just chronos-vscode's: `npm test` below builds chronos/ first, because
# test/suite.js and test/data-key-equivalence-test.mjs derive their expected
# nested-source data keys FROM chronos/dist rather than hardcoding them — a
# missing dist fails opaquely inside the extension host, and a stale one makes
# those tests agree with themselves.
- name: Install chronos (the agent package the tests derive expectations from)
working-directory: chronos
run: npm ci

- name: Install chronos-vscode
working-directory: chronos-vscode
Expand All @@ -73,8 +93,9 @@ jobs:
- name: Install xvfb
run: sudo apt-get update && sudo apt-get install -y xvfb

# npm test = esbuild bundle + run-ui-test.mjs. The test drives a deterministic
# mock pi (no real agent / API keys) and downloads VS Code into .vscode-test/.
# npm test = build chronos/ + esbuild bundle + host tests + run-ui-test.mjs.
# The UI test drives a deterministic mock pi (no real agent / API keys) and
# downloads VS Code into .vscode-test/.
- name: Run UI test
working-directory: chronos-vscode
run: xvfb-run -a npm test
13 changes: 11 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ dist/
*.js.map
chronos-vscode/node_modules/
chronos-vscode/out/
chronos-vscode/.vscode-test/
# VS Code test-electron download (appears wherever the UI test is run from)
.vscode-test/
*.tgz
*.vsix
*.html
paper.md
win-vm.md
soduco/
assets/originals/
assets/originals/
# Workspace artifacts from dev-running the agent inside the repo
/memory/
/chronos-vscode/memory/
# Implementation plans are working notes for a single change, not product docs —
# they went stale the moment their branch landed and one of them was the largest
# file in its own PR. Kept on disk, out of git. Specs (docs/superpowers/specs/)
# stay tracked: they record WHY a design was chosen.
docs/superpowers/plans/
30 changes: 26 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,35 @@ cd chronos-vscode && npx tsc --noEmit -p tsconfig.json # host (src/)
cd chronos-vscode && npx tsc --noEmit -p webview/tsconfig.json # webview/
```

Tests live in `chronos-vscode/` (no unit-test runner; two node scripts):
There is no unit-test runner — tests are plain node scripts, split across both packages. Each
package's `npm test` runs its own set:

```bash
node scripts/rpc-spike.mjs [path-to-pi] # canary: asserts the pi --mode rpc JSONL contract. Run after upgrading the global pi.
node test/run-ui-test.mjs # launches VS Code against a fixture workspace; asserts panel + webview + RPC boot
cd chronos && npm test # builds dist/, then all four agent canaries
cd chronos-vscode && npm test # builds the agent + bundle, then host tests + the UI test
```

Individually:

```bash
# chronos/ — canaries import the BUILD OUTPUT (dist/), so they need `npm run build` first
node scripts/downscale-canary.mjs # image long-edge cap
node scripts/retry-canary.mjs # expert retry/backoff policy
node scripts/timeout-canary.mjs # per-attempt timeout enforced by abort, not pi-ai timeoutMs
node scripts/collection-canary.mjs # collection context, ids, session sidecar, source precedence

# chronos-vscode/
node scripts/rpc-spike.mjs [path-to-pi] # asserts the pi --mode rpc JSONL contract. Run after upgrading the global pi.
node test/collection-id-test.mjs # host reads collection id vs display name
node test/data-key-equivalence-test.mjs # host's data-key mirror vs the agent's real derivation (needs chronos/dist)
node test/run-ui-test.mjs # launches VS Code against a fixture workspace; panel + webview + RPC boot + nested sources
```

`test/suite.js` and `test/data-key-equivalence-test.mjs` import `chronos/dist/` to derive their
expected values instead of hardcoding them, so **a stale `dist` makes them agree with themselves**.
`npm test` builds the agent first for that reason; if you invoke the scripts directly, build it
yourself. Note `chronos/dist` is gitignored and survives branch switches.

See `chronos-vscode/TESTING.md` for the manual smoke checklist.

## Architecture — the big picture
Expand Down Expand Up @@ -69,7 +91,7 @@ Key tools: `task`/`task_batch` (spawn persistent vision-expert subagents per pag

### Workspace layout (user-facing, created by `Chronos: Init Workspace`)

A Chronos *workspace* (separate from this repo) contains `sources/<name>/png/page_NNNN.png`, `data/` (outputs), `memory/` (`MEMORY.MD` + per-source `.md`, injected into the system prompt), `skills/<name>/SKILL.md`, `sessions/`, and `.chronos/` (`.env` with provider API keys e.g. `ANTHROPIC_API_KEY`/`GEMINI_API_KEY`, written by the panel's "Log in" flow; `settings.json` for the `yolo` flag; `session-sources.json` mapping session id → selected source for resume; `session-names.json` caching auto-generated session titles). The workspace `skills/` dir is bridged into pi via `.pi/settings.json` (`{ "skills": ["../skills"] }`).
A Chronos *workspace* (separate from this repo) contains `sources/<name>/png/page_NNNN.png`, `data/` (outputs), `memory/` (`MEMORY.MD` + per-source `.md`, injected into the system prompt), `skills/<name>/SKILL.md`, `sessions/`, and `.chronos/` (`.env` with provider API keys e.g. `ANTHROPIC_API_KEY`/`GEMINI_API_KEY`, written by the panel's "Log in" flow; `settings.json` for the `yolo` flag; `session-collections.json` mapping session id → `{ name?, extraMembers? }` — the selected collection plus any sources added mid-session via `change_source`, replayed on resume; `session-names.json` caching auto-generated session titles). The workspace `skills/` dir is bridged into pi via `.pi/settings.json` (`{ "skills": ["../skills"] }`).

## Reference

Expand Down
134 changes: 134 additions & 0 deletions DEV.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Development setup

How to work on Chronos **and** test the released/marketplace build without ever
uninstalling `pi` or the pi-package. The two modes are isolated, so they coexist.

## Why the two modes used to fight

Everything Chronos-related lives in one global slot per axis, so switching modes
meant mutating shared state (hence the uninstall/reinstall churn):

| Axis | Where it lives |
|------|----------------|
| `pi` binary | `~/.npm-global/bin/pi` (one global npm install) |
| agent home: package registration, `auth.json`, `models.json`, sessions | `~/.pi/agent/` |
| VS Code extension | the editor's one extension slot |

The fix isolates each axis so **local dev** and **release testing** never touch
each other's state.

## Mode 1 — local development (default)

Runs your working copy of both the extension and the agent.

**One-time:** register the agent pi-package as a local checkout. It's already set
if `~/.pi/agent/settings.json` `packages` contains a path ending in `/chronos`:

```jsonc
// ~/.pi/agent/settings.json
{ "packages": ["/abs/path/to/chronos/chronos"] }
```

The extension treats a local-checkout registration as sacred — its bootstrap
never reinstalls the pinned release on top of it (`extension.ts` →
`hasLocalChronosCheckout`). So this survives extension upgrades.

**Run the extension from source** (never install a .vsix for dev): open
`chronos-vscode/` in VS Code and press **F5** ("Run Extension" — see
`.vscode/launch.json`). This launches an Extension Development Host running your
built `out/`.

**Iterate:**

```bash
# agent (chronos/) — pi loads from dist/, so you MUST build; then restart the session
cd chronos && npm run build

# extension + webview (chronos-vscode/) — rebuild, then reload the dev-host window
cd chronos-vscode && npm run watch # or: npm run build
```

- Agent changes: `npm run build`, then **restart the pi session** (sessions
snapshot the agent at startup). Prompts/skills are read live — no build needed.
- Extension/webview changes: rebuild, then **Reload Window** in the dev host.

Test the agent alone (no extension) in a terminal from a workspace: just `pi`.

## Mode 2 — testing the released / marketplace build

Runs the packaged `.vsix` and the GitHub-pinned agent against an **isolated agent
home**, so your dev registration in `~/.pi/agent` is untouched.

**Set up a dedicated VS Code profile** (profiles isolate the installed extension
*and* settings):

```bash
# build + package the extension (or grab the marketplace .vsix)
cd chronos-vscode && npm run package # -> chronos-<version>.vsix

# create the profile and install the .vsix into it
code --profile chronos-release --install-extension chronos-vscode/chronos-*.vsix
```

**Point that profile at an isolated agent home.** In the `chronos-release`
profile's user settings (`Preferences: Open User Settings (JSON)` while in that
profile):

```jsonc
{ "chronos.piAgentDir": "~/.pi-release/agent" }
```

That's the whole trick. The extension both *reads* package registration / auth /
sessions from that dir and *passes it to the pi subprocess* as
`PI_CODING_AGENT_DIR`, so the two always agree. On first launch there, the
bootstrap sees no Chronos package in the isolated home and installs the release
pinned to the extension version (`v<version>` tag) — leaving `~/.pi/agent` alone.

A setting (not an env var) is used because VS Code doesn't reliably propagate a
launcher's environment to an already-running instance, whereas per-profile
settings always apply.

### Terminal-only release testing

To exercise the *released agent* from a terminal without the extension, use the
wrapper — it runs the global `pi` against the same isolated home:

```bash
dev/pi-release install https://github.com/ai-historian/[email protected] # one-time
dev/pi-release # run a session
```

Override the location with `PI_RELEASE_AGENT_DIR`.

## Other dev overrides (extension settings)

Machine-scoped, so they don't travel with a committed workspace. Set per-profile.

| Setting | Purpose |
|---------|---------|
| `chronos.piAgentDir` | Relocate the agent home (isolation — above). |
| `chronos.piPath` | Use a specific `pi` binary (e.g. a dev build / fork). |
| `chronos.piPackageSource` | Install the agent pkg from a local path or `<url>@branch` instead of the pinned release. |
| `chronos.piNpmPackage` | Swap the npm package for the `pi` CLI itself (e.g. a fork or pinned version). |

## Which mode am I in?

```bash
# dev home
cat ~/.pi/agent/settings.json | grep -A3 packages
# release home
cat ~/.pi-release/agent/settings.json | grep -A3 packages
```

A path entry ⇒ local dev checkout; a `github.com/...@vX` entry ⇒ pinned release.

## Typecheck / test reminders

esbuild does **not** type-check. After editing extension/webview TS:

```bash
cd chronos-vscode && npx tsc --noEmit -p tsconfig.json # host (src/)
cd chronos-vscode && npx tsc --noEmit -p webview/tsconfig.json # webview/
```

See `chronos-vscode/TESTING.md` for the RPC canary and UI-boot tests.
37 changes: 36 additions & 1 deletion chronos-vscode/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,43 @@ fact that slash-command `prompt` responses arrive only after the handler
finishes). Run this after upgrading the global `pi` install to detect protocol
drift. Last verified: pi 0.79.1 (2026-06-11).

## Agent canaries (`chronos/`)

```
cd ../chronos && npm test
```

Builds `dist/` then runs all four canaries — image downscaling, expert
retry/backoff, per-attempt timeout-as-abort, and collection context (ids,
session sidecar, source precedence). They import the **build output**, so the
build step is not optional.

## Host tests

```
node test/collection-id-test.mjs # collection id vs display name
node test/data-key-equivalence-test.mjs # host data-key mirror vs the agent's real derivation
```

`data-key-equivalence-test.mjs` imports the agent's compiled `deriveRef`/
`dataKeyForRef` from `chronos/dist` and compares them against the host's
`src/panel/data-key.ts` mirror over a case table, with no expected-value literal
— so the unavoidable duplication across the two packages is self-policing.
It needs `chronos/dist` built (`npm run build:agent`).

## VS Code integration test

```
npm test # builds, then runs test/run-ui-test.mjs
npm test # builds the agent + bundle, then host tests, then the UI test
node test/run-ui-test.mjs
```

`npm test` builds `chronos/` first because `test/suite.js` derives its expected
nested-source data keys from `chronos/dist` rather than hardcoding them — a
**stale or missing `dist` makes the test agree with itself** (or fail with an
opaque module error). `chronos/dist` is gitignored and survives branch switches,
so it can silently hold another branch's build.

Launches VS Code (local binary) with the dev extension against a fixture
workspace and drives it against **`test/mock-pi.mjs`** — a stub that speaks the
`pi --mode rpc` JSONL contract, so no real pi binary or API keys are needed. The
Expand All @@ -39,6 +69,11 @@ via a test-only `__test/invoke` / `__test/dump` message pair (see
`webview-protocol.ts`); the mock varies behavior by prompt prefix
(`select:` / `tool:` / anything → echo).

**Known flake:** this test failed roughly 1 run in 4 on unchanged code when last
measured (2026-07-28) — it drives a real VS Code instance with `waitFor` polling,
so it is timing-sensitive. Treat a single red run as inconclusive: re-run before
concluding a change broke it, and do not treat one green run as a release gate.

## Manual smoke checklist (combined viewer + chat UI)

Chat is the only UI. The automated test above covers prompt/assistant rendering,
Expand Down
43 changes: 42 additions & 1 deletion chronos-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,46 @@
"default": "",
"scope": "machine",
"markdownDescription": "**Dev override.** npm package the setup flow installs for the `pi` CLI when pi isn't found. Leave empty for the default `@earendil-works/pi-coding-agent` (the maintained package). Set to override it — e.g. a fork or a version-pinned tag like `@earendil-works/[email protected]`."
},
"chronos.piAgentDir": {
"type": "string",
"default": "",
"scope": "machine",
"markdownDescription": "**Dev override.** Relocates pi's agent home (package registration, `auth.json`, `models.json`, sessions) away from the default `~/.pi/agent`. Accepts an absolute path or a `~/`-prefixed one (e.g. `~/.pi-release/agent`). Set this in a dedicated VS Code profile to test the marketplace build against an isolated agent home without disturbing your local-dev registration in `~/.pi`. The extension both reads this location and passes it to the pi subprocess as `PI_CODING_AGENT_DIR`. Leave empty to use `PI_CODING_AGENT_DIR` from the environment, or the default `~/.pi/agent`."
},
"chronos.maxExpertToolCalls": {
"type": "integer",
"default": 100,
"minimum": 1,
"maximum": 1000,
"markdownDescription": "Maximum number of tool calls a single vision-expert subagent may make while working on one page before it is forced to answer. Higher values let experts self-zoom and explore more (better on dense or damaged pages) at higher cost and latency; lower values keep them terse and cheap. Applies to the `task` / `task_batch` experts. Default: 100."
},
"chronos.maxConcurrency": {
"type": "integer",
"default": 20,
"minimum": 1,
"maximum": 250,
"markdownDescription": "Maximum number of expert subagents `task_batch` runs in parallel. This is both the default when the agent doesn't specify a concurrency and a hard ceiling on what it may request. Higher values finish batches faster but consume more concurrent provider quota (and may hit rate limits). Default: 20."
},
"chronos.maxImageDimension": {
"type": "integer",
"default": 2576,
"minimum": 0,
"markdownDescription": "Maximum long-edge size in **pixels** for images sent to expert vision models (`task`/`task_batch` page context, `view_page`, `view_region`). Larger images are downscaled before upload — providers resize past their own caps anyway (Anthropic: 2576 px), so bigger uploads only waste bandwidth. Lower values shrink uploads roughly quadratically (1568 ≈ 4× smaller than a typical 3000 px scan) at the cost of full-page detail; experts can still zoom via `view_region`, which crops from the full-resolution file on disk. `0` sends originals untouched. Default: 2576."
},
"chronos.expertRetries": {
"type": "integer",
"default": 3,
"minimum": 0,
"maximum": 10,
"markdownDescription": "How many times a failed expert LLM call is retried (exponential backoff ≈2 s/8 s/30 s with jitter) before the page is reported failed. Retries recover pages from transient provider/network failures — timeouts, rate limits, overloads; auth and validation errors are never retried. `0` disables retries. Default: 3."
},
"chronos.expertRequestTimeout": {
"type": "integer",
"default": 300,
"minimum": 0,
"maximum": 3600,
"markdownDescription": "Per-attempt timeout in **seconds** for expert LLM calls. Bounds how long a stalled upload or an idle response stream can hold a `task_batch` concurrency slot. `0` uses the provider SDK default (typically 10 minutes). Default: 300."
}
}
},
Expand Down Expand Up @@ -138,7 +178,8 @@
"build": "node esbuild.mjs",
"watch": "node esbuild.mjs --watch",
"package": "vsce package",
"test": "node esbuild.mjs && node test/run-ui-test.mjs"
"build:agent": "npm --prefix ../chronos run build",
"test": "npm run build:agent && node esbuild.mjs && node test/collection-id-test.mjs && node test/data-key-equivalence-test.mjs && node test/run-ui-test.mjs"
},
"dependencies": {
"dompurify": "^3.4.9",
Expand Down
Loading
Loading