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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ jobs:
git config --global user.email "ci@openscience.dev"
git config --global user.name "OpenScience CI"
git config --global init.defaultBranch main
- name: Install and verify Linux sandbox
run: |
sudo apt-get update
sudo apt-get install --yes bubblewrap
# Ubuntu 24.04's host-wide AppArmor policy blocks unprivileged user
# namespaces on the hosted runner before bubblewrap can apply our
# stricter per-process profile. This runner is disposable; enable
# user namespaces for the job, then prove the sandbox can start.
if [[ -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then
echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
fi
bwrap --ro-bind / / --dev /dev --proc /proc --unshare-pid --die-with-parent -- true
- name: Build embedded web assets for server tests
run: |
bun run --cwd frontend/workspace build
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ jobs:
- name: Setup Bun
uses: ./.github/actions/setup-bun

- name: Install and verify Linux sandbox
if: matrix.settings.name == 'linux'
run: |
sudo apt-get update
sudo apt-get install --yes bubblewrap
# Ubuntu 24.04's host-wide AppArmor policy blocks unprivileged user
# namespaces on the hosted runner before bubblewrap can apply our
# stricter per-process profile. This runner is disposable; enable
# user namespaces for the job, then prove the sandbox can start.
if [[ -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then
echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
fi
bwrap --ro-bind / / --dev /dev --proc /proc --unshare-pid --die-with-parent -- true

- name: Install Playwright browsers
working-directory: frontend/workspace
run: ${{ matrix.settings.playwright }}
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/npm-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,18 @@ jobs:
- name: Install Playwright browsers
working-directory: frontend/workspace
run: bunx playwright install --with-deps
- name: Install and verify Linux sandbox
run: |
sudo apt-get update
sudo apt-get install --yes bubblewrap
# Ubuntu 24.04's host-wide AppArmor policy blocks unprivileged user
# namespaces on the hosted runner before bubblewrap can apply our
# stricter per-process profile. This runner is disposable; enable
# user namespaces for the job, then prove the sandbox can start.
if [[ -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then
echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
fi
bwrap --ro-bind / / --dev /dev --proc /proc --unshare-pid --die-with-parent -- true
- name: Install published OpenScience test package
run: |
set -euo pipefail
Expand Down
41 changes: 18 additions & 23 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,22 @@ The CLI uses a **dual-layer prompt system**: provider-level system prompts + age
```
User request with agent name (e.g., "research")
├─ Layer 1: SYSTEM role ← session prompt (provider-specific)
│ src/session/system.ts selects by model provider
├─ Layer 1: SYSTEM role ← provider-neutral product contract
│ src/session/system.ts supplies one contract to every model
└─ Layer 2: USER role injection ← agent prompt (task-specific)
src/session/prompt.ts selects by agent name + tier
```

### Session prompts (`src/session/prompt/`) (6 provider + 4 utility)
### Session prompts (`src/session/prompt/`)

| File | Purpose |
| ----------------------------------------- | ---------------- |
| `anthropic.txt` | Claude models |
| `beast.txt` | GPT-4o / o1 / o3 |
| `codex_header.txt` | GPT-5 / Codex |
| `gemini.txt` | Gemini models |
| `qwen.txt` | Qwen / fallback |
| `copilot-gpt-5.txt` | Copilot GPT-5 |
| `plan.txt`, `plan-reminder-anthropic.txt` | Plan mode |
| `build-switch.txt`, `max-steps.txt` | Utility |
| File | Purpose |
| ----------------------------------- | ---------------------------------------- |
| `core.txt` | Provider-neutral operating contract |
| `plan.txt` | Read-only Plan mode contract |
| `build-switch.txt`, `max-steps.txt` | Mode transition and step-limit utilities |

Routing logic: `src/session/system.ts` → `SystemPrompt.provider(model)`.
Routing logic: `src/session/system.ts` supplies the same product contract to every model.

### Agent prompts (`src/agent/prompt/`)

Expand Down Expand Up @@ -103,18 +98,18 @@ Custom agents can be added via config file (`openscience.json` → `agent` key).

1. **Which agent is active?** → `src/agent/agent.ts`, find the agent by name, check its `mode`, `model`, `prompt` fields
2. **Which prompt is injected?** → `src/session/prompt.ts`, follow the `input.agent.name` switch
3. **Which system prompt?** → `src/session/system.ts`, `SystemPrompt.provider(model)` selects by provider
3. **Which system prompt?** → `src/session/system.ts`, `SystemPrompt.provider(model)` returns the shared product contract

### Common failure patterns:

| Symptom | Likely cause | Where to look |
| -------------------------- | ------------------------------------------------ | ------------------------------------------------------------------- |
| Agent ignores skills | Skill catalog missing/truncated in prompt | `src/agent/prompt/{agent}.txt`, check toolkit section |
| Wrong model used | Agent/model config incorrect | `src/agent/agent.ts` + `openscience.json` `agent` config |
| Agent skips stages | Stage gates not mandatory in prompt | `src/agent/prompt/{agent}.txt`, check BLOCKING vs advisory language |
| Critique not triggered | Critique is advisory, not mandatory | `src/agent/prompt/critique.txt` + parent prompt's critique section |
| Sub-agent returns empty | Context window exhaustion or bad prompt | `src/agent/agent.ts`, check subagent's `steps` limit |
| Custom agent not appearing | Config not in `openscience.json` or wrong `mode` | Config file `agent` key → `src/agent/agent.ts` |
| Symptom | Likely cause | Where to look |
| ------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------- |
| Agent over-processes a simple request | Workflow prompt is too procedural | `src/agent/prompt/{agent}.txt`, preserve adaptive behavior |
| Wrong model used | Agent/model config incorrect | `src/agent/agent.ts` + `openscience.json` `agent` config |
| Agent delegates excessively | Task contract or prompt lost the zero-child default | `src/tool/task.txt` + `src/session/prompt/core.txt` |
| Review runs on trivial work | Review threshold is too broad | `src/agent/prompt/{agent}.txt` + `reviewer.txt` |
| Sub-agent returns empty | Context window exhaustion or bad prompt | `src/agent/agent.ts`, check subagent's `steps` limit |
| Custom agent not appearing | Config not in `openscience.json` or wrong `mode` | Config file `agent` key → `src/agent/agent.ts` |

### Key files for prompt debugging (read these first):

Expand Down
34 changes: 0 additions & 34 deletions backend/cli/bin/openscience
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,6 @@ function run(target) {
process.exit(1)
}

// Clean up files that confuse Bun compiled binaries.
// Install dir stays as ~/.openscience/ until the path-migration follow-up PR.
const openscienceDir = path.join(os.homedir(), ".openscience")
for (const poison of ["package.json", ".gitignore", "bun.lockb", "bunfig.toml"]) {
try {
fs.unlinkSync(path.join(openscienceDir, poison))
} catch {}
}
try {
fs.rmSync(path.join(openscienceDir, "node_modules"), { recursive: true })
} catch {}
// Clear macOS extended attributes that cause Bun binaries to hang
if (os.platform() === "darwin") {
try {
childProcess.spawnSync("xattr", ["-rc", openscienceDir], { stdio: "ignore" })
} catch {}
}

const result = childProcess.spawnSync(target, process.argv.slice(2), {
stdio: "inherit",
})
Expand Down Expand Up @@ -206,22 +188,6 @@ function main() {
current = parent
}

// Check ~/.openscience/bin (curl installer fallback; dir renamed later)
const homeBin = path.join(os.homedir(), ".openscience", "bin", binary)
if (isBinary(homeBin)) {
run(homeBin)
}

// Check /opt/homebrew/bin (macOS)
if (os.platform() === "darwin" && isBinary("/opt/homebrew/bin/openscience")) {
run("/opt/homebrew/bin/openscience")
}

// Check /usr/local/bin
if (isBinary("/usr/local/bin/openscience")) {
run("/usr/local/bin/openscience")
}

const expectedPackages = expectedPlatformPackages(platform, arch, musl)
console.error(`OpenScience binary not found for ${platform}-${arch}.`)
console.error(`Detected runtime: ${runtimeDescription(platform, arch, musl)}`)
Expand Down
81 changes: 9 additions & 72 deletions backend/cli/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ process.chdir(dir)
import pkg from "../package.json"
import { Script } from "@synsci/script"
import { assertLinuxArm64PageSize } from "./linux-arm64-page-size"
import { NativeTargets, nativePackageName } from "./native-targets"

// Fetch and generate models.dev snapshot. Runtime refreshes stale snapshots
// when current frontier IDs are missing, so avoid inventing model aliases here.
Expand All @@ -33,67 +34,8 @@ const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")

const allTargets: {
os: string
arch: "arm64" | "x64"
abi?: "musl"
avx2?: false
}[] = [
{
os: "linux",
arch: "arm64",
},
{
os: "linux",
arch: "x64",
},
{
os: "linux",
arch: "x64",
avx2: false,
},
{
os: "linux",
arch: "arm64",
abi: "musl",
},
{
os: "linux",
arch: "x64",
abi: "musl",
},
{
os: "linux",
arch: "x64",
abi: "musl",
avx2: false,
},
{
os: "darwin",
arch: "arm64",
},
{
os: "darwin",
arch: "x64",
},
{
os: "darwin",
arch: "x64",
avx2: false,
},
{
os: "win32",
arch: "x64",
},
{
os: "win32",
arch: "x64",
avx2: false,
},
]

const targets = singleFlag
? allTargets.filter((item) => {
? NativeTargets.filter((item) => {
if (item.os !== process.platform || item.arch !== process.arch) {
return false
}
Expand All @@ -111,7 +53,7 @@ const targets = singleFlag

return true
})
: allTargets
: NativeTargets

await $`rm -rf dist`

Expand All @@ -131,7 +73,10 @@ if (!fs.existsSync(path.join(repoRoot, "node_modules")) || !fs.existsSync(path.j
await $`bun install`.cwd(repoRoot)
}
console.log("building frontend/workspace (openscience web)")
await $`bun run build`.cwd(webAppDir)
await $`VITE_OPENSCIENCE_VERSION=${Script.version} bun run build`.cwd(webAppDir)
await Bun.file(path.join(webAppDir, "dist", "version.json")).write(
JSON.stringify({ version: Script.version, channel: Script.channel }, null, 2) + "\n",
)
await $`bun run script/generate-web-assets.ts`

// Generate encryption key for prompt files (unique per build)
Expand All @@ -143,16 +88,7 @@ if (!skipInstall) {
await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
}
for (const item of targets) {
const name = [
pkg.name,
// changing to win32 flags npm for some reason
item.os === "win32" ? "windows" : item.os,
item.arch,
item.avx2 === false ? "baseline" : undefined,
item.abi === undefined ? undefined : item.abi,
]
.filter(Boolean)
.join("-")
const name = nativePackageName(pkg.name, item)
console.log(`building ${name}`)
await $`mkdir -p dist/${name}/bin`

Expand All @@ -178,6 +114,7 @@ for (const item of targets) {
OPENSCIENCE_VERSION: `'${Script.version}'`,
OPENSCIENCE_CHANNEL: `'${Script.channel}'`,
OPENSCIENCE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
OPENSCIENCE_PLATFORM_PACKAGE: `'${name}'`,
},
})

Expand Down
72 changes: 72 additions & 0 deletions backend/cli/script/native-targets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export type NativeTarget = {
os: string
arch: "arm64" | "x64"
abi?: "musl"
avx2?: false
}

export const NativeTargets: readonly NativeTarget[] = [
{
os: "linux",
arch: "arm64",
},
{
os: "linux",
arch: "x64",
},
{
os: "linux",
arch: "x64",
avx2: false,
},
{
os: "linux",
arch: "arm64",
abi: "musl",
},
{
os: "linux",
arch: "x64",
abi: "musl",
},
{
os: "linux",
arch: "x64",
abi: "musl",
avx2: false,
},
{
os: "darwin",
arch: "arm64",
},
{
os: "darwin",
arch: "x64",
},
{
os: "darwin",
arch: "x64",
avx2: false,
},
{
os: "win32",
arch: "x64",
},
{
os: "win32",
arch: "x64",
avx2: false,
},
]

export function nativePackageName(root: string, target: NativeTarget) {
return [
root,
target.os === "win32" ? "windows" : target.os,
target.arch,
target.avx2 === false ? "baseline" : undefined,
target.abi,
]
.filter(Boolean)
.join("-")
}
Loading
Loading