Skip to content
Open
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
22 changes: 14 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,18 +272,23 @@ When sending messages to other SBs via `send_to_inbox`, use `threadKey` to maint
| `task:<id>` | PCP task coordination | `task:abc123` |
| `thread:<slug>` | Multi-step conversation with no natural key | `thread:perf-audit` |

### Cross-Project threadKeys
### Cross-Project threadKeys (MANDATORY outside Inkwell)

When working across multiple repos/projects, prefix the threadKey with the project name to avoid collisions. The format is `<project>:<type>:<identifier>`.

| Context | threadKey |
| ----------------------------- | ------------------------- |
| PR in Inkwell (this repo) | `pr:389` |
| PR in a different project | `inktrade:pr:42` |
| Issue in another project | `openclaw:issue:15` |
| Cross-project spec discussion | `inktrade:spec:valuation` |
| Context | threadKey |
| ----------------------------- | ------------------------------- |
| PR in Inkwell (this repo) | `pr:389` |
| PR in a different project | `inktrade:pr:42` |
| Issue in another project | `openclaw:issue:15` |
| Cross-project spec discussion | `inktrade:spec:valuation` |
| Branch in another project | `inktrade:branch:supabase-auth` |

Within the Inkwell repo, the project prefix is optional — `pr:389` is unambiguous. For any thread that references work in a different repo, always include the project prefix so trigger routing and session matching work correctly across boundaries.
Within the Inkwell repo, the project prefix is optional — `pr:389` is unambiguous. For any thread that references work in a **different repo, the project prefix is REQUIRED** — `pr:12` in two repos is a routing collision, and studio route patterns (`inktrade:pr:*`) can only target project-scoped keys.

The project goes in the **prefix slot, never the identifier**: `inktrade:pr:42`, not `pr:inktrade-42` or `pr:inktrade-supabase-auth`. Baking the project into the identifier defeats pattern matching and prefix-based routing.

Each repo's AGENTS.md should carry this threadKey section so agents working there natively derive project-prefixed keys.

### Sender Rules

Expand All @@ -292,6 +297,7 @@ Within the Inkwell repo, the project prefix is optional — `pr:389` is unambigu
3. **DERIVE** the key from the most specific reference. If a PR review involves spec changes, use `pr:<number>` (the actionable unit), not `spec:<slug>`.
4. **Keep identifiers stable** — use PR numbers, not PR titles. Use spec URI slugs, not descriptions.
5. If no natural key exists for a multi-step conversation, use `thread:<short-slug>` with a descriptive slug.
6. **Prefix with the project** for any thread about work outside this repo: `<project>:<type>:<identifier>` (see Cross-Project threadKeys above).

### Recipient Rules

Expand Down
15 changes: 13 additions & 2 deletions packages/api/src/services/sessions/codex-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
ToolCall,
} from './types.js';
import { formatInjectedContext } from './context-builder.js';
import { env } from '../../config/env.js';
import { logger } from '../../utils/logger.js';
import { resolveBinaryPath, buildSpawnPath } from './resolve-binary.js';
import {
Expand Down Expand Up @@ -137,9 +138,19 @@ export class CodexRunner implements IRunner {
args.push('--json');
args.push('-c', `model_instructions_file=${promptPath}`);

// Ink session headers — Codex resolves env var names to values at runtime.
// The server key must match what's in .codex/config.toml (mcp_servers.inkwell).
// Ink MCP server — fully defined via -c overrides so the spawn never
// depends on the user's ~/.codex/config.toml having (or keeping) the
// entry. A partial entry (headers without url) makes Codex fail with
// "Error loading config.toml: invalid transport".
// Inside a Docker sandbox, loopback resolves to the container, so use
// host.docker.internal (matches the orchestrator's .mcp.json rewrite).
const codexServerKey = 'inkwell';
const mcpHost = config.container ? 'host.docker.internal' : 'localhost';
args.push(
'-c',
`mcp_servers.${codexServerKey}.url="http://${mcpHost}:${env.MCP_HTTP_PORT}/mcp"`
);
// Session headers — Codex resolves env var names to values at runtime.
args.push('-c', `mcp_servers.${codexServerKey}.env_http_headers.x-ink-context="INK_CONTEXT"`);
args.push('-c', `mcp_servers.${codexServerKey}.env_http_headers.x-ink-agent-id="AGENT_ID"`);
args.push(
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/skills/builtin/pcp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Inkwell tools are available via MCP. If the Inkwell MCP server is not already co
```json
{
"mcpServers": {
"pcp": {
"inkwell": {
"command": "node",
"args": ["/path/to/pcp/packages/api/dist/index.js"],
"env": { "MCP_TRANSPORT": "stdio" }
Expand All @@ -69,7 +69,7 @@ Inkwell tools are available via MCP. If the Inkwell MCP server is not already co
```json
{
"mcpServers": {
"pcp": {
"inkwell": {
"url": "http://localhost:3001/mcp",
"headers": {
"Authorization": "Bearer ${INK_ACCESS_TOKEN}"
Expand Down
6 changes: 3 additions & 3 deletions packages/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Inkwell desktop app — a **thin Tauri v2 native shell** over the locally runnin

## Architecture

The app does **not** bundle the web app. It opens a native window pointed at the local Next.js server (default `http://localhost:3000`, i.e. `INK_PORT_BASE - 1`; the API stays on 3001). Everything you see is the same dashboard you'd see in a browser — the desktop app just adds native chrome:
The app does **not** bundle the web app. It opens a native window pointed at the local Next.js server (default `http://localhost:3002`, i.e. `INK_PORT_BASE + 1`; the API stays on 3001). Everything you see is the same dashboard you'd see in a browser — the desktop app just adds native chrome:

- **Window** titled "Inkwell" with the standard macOS menu (Edit/Copy/Paste bindings work in the webview).
- **Server-not-running fallback**: on launch the window shows a bundled static page (`ui/index.html`) — never a blank white window. A background thread in the Rust shell polls the server's TCP port every 1.5s and navigates the webview to the dashboard the moment it's reachable.
Expand All @@ -19,11 +19,11 @@ Optional config file at `~/.ink/desktop.json`:
```json
{
"host": "localhost",
"port": 3000
"port": 3002
}
```

Both fields are optional — defaults are `localhost:3000`. If you run the dashboard on a different port base (e.g. `INK_PORT_BASE=4001` → web on 4002), set `"port": 4002`. Invalid JSON falls back to defaults (with a warning on stderr).
Both fields are optional — defaults are `localhost:3002`. If you run the dashboard on a different port base (e.g. `INK_PORT_BASE=4001` → web on 4002), set `"port": 4002`. Invalid JSON falls back to defaults (with a warning on stderr).

## Prerequisites

Expand Down
8 changes: 4 additions & 4 deletions packages/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Inkwell desktop shell.
//!
//! A thin native wrapper around the locally running Inkwell web dashboard
//! (Next.js, default `http://localhost:3000`). The app does NOT bundle the web
//! (Next.js, default `http://localhost:3002`). The app does NOT bundle the web
//! app — it points the webview at the local dev/prod server.
//!
//! On launch the window shows a bundled "waiting for server" page while a
Expand All @@ -19,15 +19,15 @@ use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowEve

const MAIN_WINDOW: &str = "main";
const DEFAULT_HOST: &str = "localhost";
/// Web dashboard default port (`INK_PORT_BASE - 1`, i.e. 3001 - 1).
const DEFAULT_PORT: u16 = 3000;
/// Web dashboard default port (`INK_PORT_BASE + 1`, i.e. 3001 + 1).
const DEFAULT_PORT: u16 = 3002;
const POLL_INTERVAL: Duration = Duration::from_millis(1500);
const CONNECT_TIMEOUT: Duration = Duration::from_millis(800);

/// Optional user config at `~/.ink/desktop.json`:
///
/// ```json
/// { "host": "localhost", "port": 3000 }
/// { "host": "localhost", "port": 3002 }
/// ```
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
<h1>Waiting for the Inkwell server<span class="dots"></span></h1>
<p>
This app is a shell over your locally running dashboard at
<span class="url" id="server-url">http://localhost:3000</span>
<span class="url" id="server-url">http://localhost:3002</span>
</p>
<p>
Start it with <code>yarn dev</code> in the Inkwell repo &mdash; this window will connect
Expand Down
4 changes: 2 additions & 2 deletions packages/openclaw-plugin/skills/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ PCP tools are available via MCP. If the PCP MCP server is not already configured
```json
{
"mcpServers": {
"pcp": {
"inkwell": {
"command": "node",
"args": ["/path/to/pcp/packages/api/dist/index.js"],
"env": { "MCP_TRANSPORT": "stdio" }
Expand All @@ -69,7 +69,7 @@ PCP tools are available via MCP. If the PCP MCP server is not already configured
```json
{
"mcpServers": {
"pcp": {
"inkwell": {
"url": "http://localhost:3001/mcp",
"headers": {
"Authorization": "Bearer ${INK_ACCESS_TOKEN}"
Expand Down
Loading