Add waiting arcade MVP

This commit is contained in:
Anthony Ettinger 2026-06-06 22:09:22 +00:00
parent b1d8fe475a
commit 4682b261c0
16 changed files with 1192 additions and 2 deletions

View file

@ -0,0 +1,87 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { homedir } from "node:os";
export type JsonObject = Record<string, unknown>;
export const defaultConfig: JsonObject = {
waiting: {
arcade: {
enabled: true,
defaultGame: "hangman",
random: false,
autoStartAfterSeconds: 0,
interruptOnApproval: true,
interruptOnDone: true,
interruptOnError: true,
showTaskStatusOverlay: true
}
}
};
export function configPath() {
return join(homedir(), ".logicsrc", "config.json");
}
export function readConfig() {
const file = configPath();
if (!existsSync(file)) {
return structuredClone(defaultConfig);
}
const parsed = JSON.parse(readFileSync(file, "utf8")) as JsonObject;
return mergeConfig(structuredClone(defaultConfig), parsed);
}
export function writeConfig(config: JsonObject) {
const file = configPath();
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
return file;
}
export function getConfigValue(path: string, config = readConfig()) {
return path.split(".").reduce<unknown>((current, key) => (isObject(current) ? current[key] : undefined), config);
}
export function setConfigValue(path: string, rawValue: string, config = readConfig()) {
const parts = path.split(".").filter(Boolean);
if (parts.length === 0) {
throw new Error("Config path cannot be empty.");
}
let current: JsonObject = config;
for (const part of parts.slice(0, -1)) {
if (!isObject(current[part])) {
current[part] = {};
}
current = current[part] as JsonObject;
}
current[parts[parts.length - 1] as string] = parseConfigValue(rawValue);
return config;
}
export function parseConfigValue(value: string): unknown {
if (value === "true") return true;
if (value === "false") return false;
if (value === "null") return null;
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
try {
return JSON.parse(value) as unknown;
} catch {
return value;
}
}
function mergeConfig(base: JsonObject, override: JsonObject): JsonObject {
for (const [key, value] of Object.entries(override)) {
if (isObject(value) && isObject(base[key])) {
base[key] = mergeConfig(base[key] as JsonObject, value);
} else {
base[key] = value;
}
}
return base;
}
function isObject(value: unknown): value is JsonObject {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

View file

@ -1,22 +1,61 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { Command } from "commander";
import { renderPluginStatus, renderTui } from "@logicsrc/tui";
import { renderArcadeList, renderPluginStatus, renderTui, runArcadeSession, type TaskSnapshot } from "@logicsrc/tui";
import { assertSchemaKind, parseDocument, validate } from "@logicsrc/validators";
import { getConfigValue, readConfig, setConfigValue, writeConfig } from "./config.js";
import { boards, tasks } from "./fixtures.js";
import { print, type OutputFormat } from "./format.js";
import { exportOpenSpecSummary, importOpenSpec, writeOpenSpecChange } from "./openspec.js";
import { defaultPluginRegistry } from "./registry.js";
process.stdout.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EPIPE") {
process.exit(0);
}
throw error;
});
const program = new Command();
program.enablePositionalOptions();
program
.name("logicsrc")
.description("LogicSRC OpenSpec CLI for schemas, boards, tasks, agents, payments, plugins, and TUI.")
.option("--openspec", "Enable OpenSpec.dev-compatible repo-local specs, proposals, tasks, and deltas where supported.")
.option("--openspec-only", "Restrict workflows to LogicSRC OpenSpec schemas, SDKs, MCP, CLI, TUI, and PWA contracts.")
.option("--yolo", "Start the default AgentSwarm YOLO flow.")
.option("--arcade [game]", "Launch Waiting Arcade while a long-running task executes.")
.option("--waiting-arcade", "Alias for --arcade.")
.option("--waiting-game <game>", "Alias for --arcade=<game>.")
.option("--no-arcade", "Disable Waiting Arcade.")
.version("0.1.0");
program.action(async (options) => {
if (!options.yolo) {
program.outputHelp();
return;
}
const arcadeGame = resolveArcadeGame(options);
if (arcadeGame) {
await runYoloArcade(arcadeGame);
return;
}
print(
{
type: "logicsrc.agentswarm.session",
status: "opening",
mode: "yolo",
master_agent: "agentswarm-master",
slave_agents: ["reproduce", "patch", "review"],
arcade: false
},
"json"
);
});
program
.command("login")
.option("--did <did>", "CoinPay DID")
@ -124,16 +163,59 @@ program.command("events").description("Listen to LogicSRC event stream.").argume
console.log(JSON.stringify({ type: "logicsrc.event", event: "task.created", resource_id: "task_789" }));
});
const arcade = program.command("arcade").description("Play Waiting Arcade games.");
arcade.action(async () => {
await runArcadeSession({ game: process.env.LOGICSRC_ARCADE_GAME || "hangman", standalone: true });
});
arcade.command("list").description("List built-in Waiting Arcade games.").action(() => {
console.log(renderArcadeList());
});
arcade
.command("play")
.argument("[game]", "Game id or random", "hangman")
.description("Play a standalone Waiting Arcade game.")
.action(async (game) => {
await runArcadeSession({ game, standalone: true });
});
const config = program.command("config").description("Read and write LogicSRC config.");
config
.command("get")
.argument("<path>", "Dot-path config key")
.description("Print a config value.")
.action((path) => {
console.log(JSON.stringify(getConfigValue(path), null, 2));
});
config
.command("set")
.argument("<path>", "Dot-path config key")
.argument("<value>", "JSON, boolean, number, or string value")
.description("Set a config value.")
.action((path, value) => {
const next = setConfigValue(path, value);
const file = writeConfig(next);
console.log(`Set ${path} in ${file}`);
});
program
.command("agentswarm")
.alias("agent-swarm")
.description("Open an AgentSwarm master agent session.")
.option("--yolo", "Start the master agent with autonomous execution enabled.")
.option("--arcade [game]", "Launch Waiting Arcade while AgentSwarm runs.")
.option("--waiting-arcade", "Alias for --arcade.")
.option("--waiting-game <game>", "Alias for --arcade=<game>.")
.option("--no-arcade", "Disable Waiting Arcade.")
.option("--repo <repo>", "Target repository, for example profullstack/logicsrc")
.option("--agents <agents>", "Comma-separated slave agent roles", "reproduce,patch,review")
.option("--change <id>", "OpenSpec-compatible change id", "agentswarm-yolo")
.option("--out <dir>", "OpenSpec-compatible output directory")
.action((options) => {
.action(async (options) => {
if (!options.yolo) {
console.log("AgentSwarm is coming soon. Run `logicsrc agentswarm --yolo` to open the master agent flow.");
return;
@ -157,6 +239,12 @@ program
})
: null;
const arcadeGame = resolveArcadeGame(options);
if (arcadeGame) {
await runYoloArcade(arcadeGame, options.repo);
return;
}
print({
type: "logicsrc.agentswarm.session",
status: "opening",
@ -284,6 +372,49 @@ function toTaskSchema(item: (typeof tasks)[number]) {
};
}
function resolveArcadeGame(options: { arcade?: string | boolean; waitingArcade?: boolean; waitingGame?: string }) {
if (options.arcade === false || process.env.LOGICSRC_NO_ARCADE === "1") {
return undefined;
}
if (options.waitingGame) {
return options.waitingGame;
}
if (typeof options.arcade === "string") {
return options.arcade;
}
const config = readConfig();
const configuredDefault = String(getConfigValue("waiting.arcade.defaultGame", config) ?? "hangman");
if (options.arcade === true || options.waitingArcade || process.env.LOGICSRC_ARCADE === "1") {
return process.env.LOGICSRC_ARCADE_GAME || configuredDefault;
}
return undefined;
}
async function runYoloArcade(game: string, repo?: string) {
const task: TaskSnapshot = {
id: "agentswarm_yolo",
title: repo ? `AgentSwarm YOLO on ${repo}` : "AgentSwarm YOLO session",
status: "running",
phase: "starting",
progress: 0.05,
costUsd: 0,
lastMessage: "launching Waiting Arcade"
};
await runArcadeSession({
game,
standalone: false,
task,
simulateTask: true,
logs: ["AgentSwarm master session started.", "Task continues while Waiting Arcade is active."]
});
}
program.parseAsync(process.argv).catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;