Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/swift-agents-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@davstack/open-agents": minor
---

Include exact current-session conversation history automatically, with task-specific curation for oversized transcripts and a `--no-history` opt-out.
43 changes: 27 additions & 16 deletions packages/init/src/skills/explore.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,43 @@ description: >-

<!-- GENERATED from skills/explore/SKILL.md by scripts/sync-init-skills.ts — DO NOT EDIT BY HAND -->

Scope tightly, then run (backgrounded — the harness notifies you):
Default to one short `--task`, especially for lookups, traces, and other simple
requests. Use the user's request verbatim when it is already concise:

explore submit --file ~/.davstack/specs/<slug>.md
explore submit --task "Find where Juno's realtime voice is locked to marin, with exact path:line citations"

When the current conversation already contains the real context, prefer compact
mode over writing a spec:
Do not create a spec file or invent `<goal>`, `<context>`, or `<scope>` merely
to wrap a task the user already stated. Conversation history supplies the
surrounding context automatically.

explore submit --compact-mode "audit supervisor handoffs"
Run one foreground submission with a long shell timeout. If the shell yields a
running handle, wait on that same handle; completion wakes the agent. Never pass
`--background`, poll `result`, or resubmit.

Keep the inline compact prompt to roughly 5-10 words. Do not paste details,
requirements, quotes, or file lists into it; the compact spec-writer reads the
current transcript/history and distills that context for the executor. Trust the
subagent handoff unless the task is genuinely too ambiguous from conversation
history.
Conversation history is included automatically. Keep the task to roughly one
sentence; do not repeat details, quotes, or file lists already present in the
conversation. History up to the direct budget goes to the executor unchanged;
oversized history is reduced to task-specific context first. Use `--no-history`
only when the conversation is irrelevant. `--compact-mode` remains a legacy
alias and is no longer needed.

For a **single scoped fact**, skip the spec file — inline it (no boilerplate):
If the conversation contains secrets, credentials, audit artifacts, or
user/health data, use `--no-history` and provide a sanitized scoped spec.

explore submit '<goal>Exact signature + return type of resolve_query_adapter</goal> <scope>backend/src/query/adapter.py only</scope>'
For a single scoped fact, use `--task` with no boilerplate:

Many `--file` run in parallel from one command. Read the `result → <path>`
file for the answer.
explore submit --task "Find the exact signature and return type of resolve_query_adapter in backend/src/query/adapter.py"

Begin every `--file` spec with a markdown `# 3-5 word title` line — a short
Use `--spec-file` only when essential instructions not present in conversation
cannot fit cleanly in one sentence, or when supplying an intentionally prepared
multi-part spec. Never generate one pre-emptively. Multiple `--task` or
`--spec-file` inputs can run in parallel.

If a spec file is genuinely needed, begin it with a markdown `# 3-5 word title`
line — a short
overview of the task. The TUI agent viewer renders this as the job label;
without it the viewer falls back to the first 5 words of the spec, which is
rarely meaningful. (Inline single-fact submits can skip the heading.)
rarely meaningful.

The spec is just goal / context (the one gotcha) / scope tags. Do NOT add an
output section — the structured `path:line` deliverable is automatic.
Expand Down
37 changes: 25 additions & 12 deletions packages/init/src/skills/fast-edit.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,41 @@ description: >-

<!-- GENERATED from skills/fast-edit/SKILL.md by scripts/sync-init-skills.ts — DO NOT EDIT BY HAND -->

Run (backgrounded — the harness notifies you):
Default to one short `--task` whenever the mechanical edit can be stated in one
sentence, especially when conversation history already contains the details:

npx fast-edit submit --file ~/.davstack/specs/<slug>.md
fast-edit submit --task "Rename fooBar to computeFoo and update its callers without changing behavior"

When the current conversation already contains the real context, prefer compact
mode over writing a spec:
Do not create a spec file or invent intent/changes/constraints tags merely to
wrap a task the user already stated. Conversation history supplies the
surrounding context automatically.

npx fast-edit submit --compact-mode "rename legacy adapter"
Run one foreground submission with a long shell timeout. If the shell yields a
running handle, wait on that same handle; completion wakes the agent. Never pass
`--background`, poll `result`, or resubmit.

Keep the inline compact prompt to roughly 5-10 words. Do not paste details,
requirements, quotes, or file lists into it; the compact spec-writer reads the
current transcript/history and distills that context for the executor. Trust the
subagent handoff unless the edit is too risky or underspecified from
conversation history.
Conversation history is included automatically. Keep the task to roughly one
sentence; do not repeat details, quotes, or file lists already present in the
conversation. History up to the direct budget goes to the executor unchanged;
oversized history is reduced to task-specific context first. Use `--no-history`
only when the conversation is irrelevant. `--compact-mode` remains a legacy
alias and is no longer needed.

**Routing test.** Delegate when a *short* intent+constraints spec is enough
If the conversation contains secrets, credentials, audit artifacts, or
user/health data, use `--no-history` and provide a sanitized scoped spec.

**Routing test.** Delegate when a short task is enough
for the executor to produce the **full** intended edit. If writing the spec
would mean pasting the new file contents or spelling out every line, the spec
costs as much as the edit — just do it yourself. (Having read the files is
fine; verbatim-detail specs are the only real waste.)

Begin every spec with a markdown `# 3-5 word title` line — a short overview of
Use `--spec-file` only when essential constraints not present in conversation
cannot fit cleanly in one sentence, or when supplying an intentionally prepared
multi-part spec. Never generate one pre-emptively.

If a spec file is genuinely needed, begin it with a markdown `# 3-5 word title`
line — a short overview of
the task. The TUI agent viewer renders this as the job label; without it the
viewer falls back to the first 5 words of the spec, which is rarely meaningful.

Expand Down
61 changes: 15 additions & 46 deletions packages/open-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ persisted and re-printable.

Not an orchestrator — the design goal is to **make a Cursor job a
self-waiting, harness-trackable command** so the harness's own
background-completion notification *is* the orchestration: no polling, no
background-completion notification _is_ the orchestration: no polling, no
status truncation, no near-miss re-send.

## Install
Expand All @@ -31,37 +31,26 @@ different profile bound (read-only vs `--force` edit).
## Verbs

```
submit --file a.md [--file b.md …] | "<inline>" [--edit] [--model m] [--timeout s] [--cwd d]
submit --task "<short instruction>" | --spec-file a.md [--spec-file b.md …] | "<inline>"
[--no-history] [--edit] [--model m] [--timeout s] [--cwd d]
--model <id> overrides the configured/provider default for this submission.
default: BLOCKS until all done, exits worst code. Each job's clean
deliverable → its OWN <id>.result.md; stdout is just an index
(`result → <path>`) — no input echo, jobs never mix. Read the file(s).
many --file ⇒ run in parallel · --detach: print bare id(s), don't wait
--task and --spec-file may repeat; many inputs run in parallel
--file remains an alias for --spec-file · --detach: print bare id(s), don't wait
--parallel-mode asap|all-together (default asap): asap prints each
index line as its job finishes; all-together waits, submission order
--compact-mode: treat the inline input as a very short task title
(roughly 5-10 words) and let a Cursor spec-writer using the built-in
default model distill
recent conversation history into the executor spec. Do not paste
details, quotes, or file lists into the inline prompt when the current
conversation already has that context.
History resolves from --history-file <path>, OPEN_AGENTS_HISTORY_FILE,
CLAUDE_CODE_TRANSCRIPT_PATH, or the current Claude Code transcript
when CLAUDE_CODE_SESSION_ID is set. It also detects Codex sessions
from CODEX_THREAD_ID / ~/.codex/sessions and falls back to
~/.codex/history.jsonl when no richer transcript is available.
Compact mode gives the spec-writer the last 50000 token-like history
units plus a pointer to the full history file, or 100000 when a local
Headroom proxy is healthy. The generated spec is kept concise and points
back to the history file for uncertain detail instead of copying the
transcript. Progress output includes the spec generation duration and
a ~/... path to the generated spec artifact.
--headroom auto|off|require (default auto): probe the local Headroom
proxy and, when healthy, run the Cursor adapter with
OPENAI_BASE_URL=http://127.0.0.1:8787/v1. If the proxy is absent,
jobs continue normally with a concise stderr notice. Use
--headroom-url <url>, OPEN_AGENTS_HEADROOM_URL, or config
headroom.url for a non-default proxy.
current-session history is automatic. Up to 100000 token-like units go
directly to each executor without a preparatory model run. Above that
budget, one foreground curator run returns an isolated relevant-history
slice for each task; the submitted task itself remains verbatim.
Exact sessions resolve from --history-file, explicit transcript env
variables, CLAUDE_CODE_SESSION_ID, or CODEX_THREAD_ID. The CLI never
guesses the newest transcript. If no exact session is available it
warns and continues task-only. --no-history disables the behavior.
--include-relevant-history and --compact-mode remain compatibility
aliases for the default behavior.
wait wait for ALL running jobs (this repo)
wait "<id…>" | <id…> wait for ALL of these
wait --any <id…> return when ≥1 done; prints which (loop = popcorn)
Expand Down Expand Up @@ -112,26 +101,6 @@ The bin launcher prefers `bun` (matches sibling davstack packages); set
`OPEN_AGENTS_RUNTIME=node` to use `node --experimental-transform-types`
instead. The source is pure `node:*` — either runtime works.

## Optional Headroom proxy

Headroom is optional. By default, `open-agents` briefly probes
`http://127.0.0.1:8787/health`. If the proxy is healthy and the selected adapter
is `cursor`, the spawned Cursor Agent process receives
`OPENAI_BASE_URL=http://127.0.0.1:8787/v1`. If the proxy is not installed or not
running, the job still runs directly.

Controls:

```bash
OPEN_AGENTS_HEADROOM=off explore submit "short task"
OPEN_AGENTS_HEADROOM=require fast-edit submit --compact-mode "fix parser"
OPEN_AGENTS_HEADROOM_URL=http://127.0.0.1:8788 explore submit "inspect auth"
```

In compact mode, a healthy Headroom proxy raises the history tail budget from
50000 to 100000 token-like units and prints a `/stats` delta from
`requests.total` and `tokens.saved`.

## Job state

Lives under `~/.davstack/jobs/<repo-hash>/` (override `OPEN_AGENTS_HOME`).
Expand Down
110 changes: 110 additions & 0 deletions packages/open-agents/__tests__/compact-background.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { spawnSync } from "node:child_process";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, test } from "vitest";
import type { JobRecord } from "../src/core/jobs.js";
import { repoHash } from "../src/core/paths.js";

const exploreEntrypoint = fileURLToPath(
new URL("../src/entrypoints/explore.ts", import.meta.url),
);

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

describe("automatic history background submission", () => {
test("accepts punctuation safely and returns one executor with direct history", async () => {
const sandbox = mkdtempSync(
join(tmpdir(), "open-agents-compact-background-"),
);
const repo = join(sandbox, "repo");
const state = join(sandbox, "state");
const history = join(sandbox, "history.jsonl");
mkdirSync(repo, { recursive: true });
writeFileSync(join(repo, "package.json"), "{}\n");
writeFileSync(
history,
`${JSON.stringify({ role: "user", content: "context" })}\n`,
);

try {
const task = `review Juno's "voice" via $path; keep | and & literal`;
const run = spawnSync(
process.execPath,
[
"--import",
"tsx",
exploreEntrypoint,
"submit",
"--task",
task,
"--history-file",
history,
"--background",
"--cwd",
repo,
],
{
cwd: dirname(dirname(dirname(exploreEntrypoint))),
encoding: "utf8",
env: {
...process.env,
OPEN_AGENTS_HOME: state,
CURSOR_AGENT_BIN: join(sandbox, "missing-agent"),
},
timeout: 10_000,
windowsHide: true,
},
);

expect(run.error).toBeUndefined();
expect(run.status).toBe(0);
const ids = run.stdout.trim().split(/\s+/).filter(Boolean);
expect(ids).toHaveLength(1);

const jobDir = join(state, "jobs", repoHash(repo));
const jobPath = join(jobDir, `${ids[0]}.json`);
expect(existsSync(jobPath)).toBe(true);
const accepted = JSON.parse(readFileSync(jobPath, "utf8")) as JobRecord;
expect(accepted.prompt).toBe(task);
expect(accepted.compactTask).toBeUndefined();
expect(accepted.historyMode).toBe("direct");
expect(accepted.fullPrompt).toContain("Read the complete delegated task");
expect(["running", "failed"]).toContain(accepted.status);

const spec = readFileSync(join(jobDir, `${ids[0]}.spec.md`), "utf8");
expect(spec).toContain(`# Authoritative task\n${task}`);
expect(spec).toContain("User:\ncontext");

let settled = accepted;
for (
let attempt = 0;
attempt < 50 && settled.status === "running";
attempt += 1
) {
await delay(100);
settled = JSON.parse(readFileSync(jobPath, "utf8")) as JobRecord;
}
expect(settled.status).toBe("failed");

const records = readdirSync(jobDir)
.filter((name) => name.endsWith(".json"))
.map(
(name) =>
JSON.parse(readFileSync(join(jobDir, name), "utf8")) as JobRecord,
);
expect(records.filter((job) => job.prompt === task)).toHaveLength(1);
} finally {
rmSync(sandbox, { recursive: true, force: true });
}
}, 20_000);
});
Loading