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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ include the full version, for example `## 0.7.0-beta.1`.

### Fixed

- Keep repository inventory RPC responses valid JSON when an owner has no runtime
snapshots. Isolate setup-test registries so validation never adds fixture
repositories to the operator's inventory.

- Use the successful session's final completion report as the PR description instead
of its initial acknowledgement. Persist reports across restarts, retain the
original summary plus the latest follow-up, distinguish dispatcher checks from
Expand Down Expand Up @@ -134,4 +138,4 @@ include the full version, for example `## 0.7.0-beta.1`.
- Add detailed bot workflow diagrams and a documentation map in `AGENTS.md`.
- Expand bundled bot instructions for planning, delegation, verification, and
handing publication back to the dispatcher.
- Add the README banner showing an OpenCode2 agent executing a task.
- Add the README banner showing an OpenCode2 agent executing a task.
3 changes: 2 additions & 1 deletion docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ Full task history remains available through `status`; see the
`opencode2-automation list [--json]` is independent of the current checkout and
service discovery. `automation.github.repositories` accepts `{}` and returns the
same `{ entries, warnings }` report on the connected server. The method reads
local registry/snapshot files; it does not invoke RPC in other owner locations,
local registry/snapshot files and omits absent snapshot fields so inactive or
missing owners also produce valid JSON. It does not invoke RPC in other owner locations,
which could activate their plugins. `/bot` → **Repositories** consumes this API.

`init` and combined-plugin activation register standard configurations. Dispatcher
Expand Down
5 changes: 4 additions & 1 deletion src/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ export async function listRepositories(now = Date.now()): Promise<RepositoryRepo
report.entries.push(row);
try {
const [d, s] = await Promise.all([readRuntime(entry.ownerDirectory, "dispatcher"), readRuntime(entry.ownerDirectory, "scheduler")]);
row.dispatcher = d?.dispatcher; row.dispatcherAt = d?.at; row.scheduler = s?.scheduler; row.schedulerAt = s?.at;
// The RPC transport validates JSON before its output schema. Absent
// snapshots must be omitted, never assigned as explicit undefined.
if (d?.dispatcher) { row.dispatcher = d.dispatcher; row.dispatcherAt = d.at; }
if (s?.scheduler) { row.scheduler = s.scheduler; row.schedulerAt = s.at; }
const fresh = (v?: Runtime) => Boolean(v && !v.stopped && alive(v.pid) && now >= v.at && now - v.at <= 15000);
if (!d || d.stopped || !alive(d.pid)) {
row.status = "not-running"; row.reason = "Configured; dispatcher is not running or has not reported since registration. Snapshots, if present, are historical.";
Expand Down
4 changes: 3 additions & 1 deletion test/repositories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ test("inventory is read-only and distinguishes active, paused, stale, stopped an
const f = await fixture(); let stopD: (() => Promise<void>) | undefined, stopS: (() => Promise<void>) | undefined;
try {
await registerRepositories([f.entry]);
assert.equal((await listRepositories()).entries[0]?.status, "not-running");
const inactive = await listRepositories();
assert.equal(inactive.entries[0]?.status, "not-running");
assert.deepEqual(inactive, JSON.parse(JSON.stringify(inactive)), "RPC output must contain JSON values only, even before activation");
stopD = publishRepositoryRuntime(f.project, "dispatcher", () => f.dispatcher);
stopS = publishRepositoryRuntime(f.project, "scheduler", () => f.scheduler);
await waitFor(async () => (await listRepositories()).entries[0]?.status === "running");
Expand Down
8 changes: 4 additions & 4 deletions test/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ test("configuration command writes one field and never overwrites existing setti
const mock = join(dir, "github-mock.mjs");
await writeFile(mock, 'globalThis.fetch = async url => { if (!String(url).startsWith("https://api.github.com/")) throw new Error("Unexpected network request"); return Response.json(String(url).endsWith("/user") ? {login:"alice"} : {default_branch:"main"}); };');
const args = ["--import", import.meta.resolve("tsx"), "--import", mock, fileURLToPath(new URL("../src/setup.ts", import.meta.url)), "init", "--model", "provider/model", "--yes"];
const result = await exec(process.execPath, args, { cwd: dir, env: { ...process.env, GITHUB_TOKEN: "fixture-secret" } });
const result = await exec(process.execPath, args, { cwd: dir, env: { ...process.env, XDG_STATE_HOME: join(dir, "state"), GITHUB_TOKEN: "fixture-secret" } });
assert.match(result.stdout, /Ready: owner\/repo/);
assert.ok(!result.stdout.includes("fixture-secret"));
const path = join(dir, ".opencode", "automation.json");
assert.deepEqual(JSON.parse(await readFile(path, "utf8")), { model: "provider/model" });
await rm(path);
await rm(join(dir, "package.json"));
const skipped = await exec(process.execPath, [...args, "--skip-tests"], { cwd: dir, env: { ...process.env, GITHUB_TOKEN: "fixture-secret" } });
const skipped = await exec(process.execPath, [...args, "--skip-tests"], { cwd: dir, env: { ...process.env, XDG_STATE_HOME: join(dir, "state"), GITHUB_TOKEN: "fixture-secret" } });
assert.match(skipped.stdout, /skipped/);
assert.deepEqual(JSON.parse(await readFile(path, "utf8")), { model: "provider/model", check: false });
await writeFile(path, JSON.stringify({ model: "provider/model" }));
await writeFile(join(dir, "package.json"), JSON.stringify({ scripts: { test: "node --test" } }));
await assert.rejects(exec(process.execPath, args, { cwd: dir, env: { ...process.env, GITHUB_TOKEN: "fixture-secret" } }));
await assert.rejects(exec(process.execPath, args, { cwd: dir, env: { ...process.env, XDG_STATE_HOME: join(dir, "state"), GITHUB_TOKEN: "fixture-secret" } }));
assert.deepEqual(JSON.parse(await readFile(path, "utf8")), { model: "provider/model" });
} finally { await rm(dir, { recursive: true, force: true }); }
});
Expand All @@ -43,7 +43,7 @@ test("interactive CLI saves account-derived defaults and displays English prompt
const mock = join(dir, "interactive-mock.mjs");
await writeFile(mock, 'Object.defineProperty(process.stdin,"isTTY",{value:true});globalThis.fetch=async url=>{if(!String(url).startsWith("https://api.github.com/"))throw new Error("Unexpected network request");return Response.json(String(url).endsWith("/user")?{login:"alice"}:{default_branch:"main"})};');
const output = await new Promise<string>((resolve, reject) => {
const child = spawn(process.execPath, ["--import", import.meta.resolve("tsx"), "--import", mock, fileURLToPath(new URL("../src/setup.ts", import.meta.url)), "init", "--model", "provider/model", "--capabilities", "text,vision", "--base-branch", "main"], { cwd: dir, env: { ...process.env, GITHUB_TOKEN: "fixture-secret" }, stdio: ["pipe", "pipe", "pipe"] });
const child = spawn(process.execPath, ["--import", import.meta.resolve("tsx"), "--import", mock, fileURLToPath(new URL("../src/setup.ts", import.meta.url)), "init", "--model", "provider/model", "--capabilities", "text,vision", "--base-branch", "main"], { cwd: dir, env: { ...process.env, XDG_STATE_HOME: join(dir, "state"), GITHUB_TOKEN: "fixture-secret" }, stdio: ["pipe", "pipe", "pipe"] });
let output = "", error = "", pending = "";
const timer = setTimeout(() => { child.kill(); reject(new Error("Wizard timed out")); }, 15000);
child.stdout.on("data", chunk => {
Expand Down