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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ packages/
openontology OpenOntology reference engine (entities, claims, queries, change sets)
openprd OpenPRD reference implementation (numbered PRDs, lifecycle, task bridge)
openfleet OpenFleet reference implementation (record, ledger, sysop verbs, Claude Code hooks)
openerrand OpenErrand reference runner (errand files in headless Chrome, human gates kept human)
logicsrc-mcp @profullstack/logicsrc-mcp standards MCP server
sdk SDK contract types and helpers
tui terminal UI
Expand Down Expand Up @@ -167,6 +168,7 @@ logicsrc prd … # OpenPRD
logicsrc ontology … # OpenOntology
logicsrc context … # OpenContext (also `opencontext`)
logicsrc fleet … # OpenFleet: open, cap, tree, stop, log, hooks install
logicsrc errand … # OpenErrand: run, validate, status (an errand on a site with no API, gates kept human)
logicsrc openmcp … # OpenMCP: relays, find, call, add, probe, serve (also `openmcp`)
logicsrc openspec … # import, export, change; any other word is OpenSpec.dev's own CLI (init, list, validate, archive, show)
logicsrc mcp # the LogicSRC MCP server over stdio (also `logicsrc-mcp`)
Expand Down
5 changes: 3 additions & 2 deletions apps/logicsrc-web/src/app/openerrand/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ export default function OpenErrandPage(): ReactNode {
values that are secret, the steps that belong to a person, and the point where it stops.
</p>
<p style={{ color: "#5b6b7a" }}>
Status: 0.1. A generic runner, <code style={mono}>logicsrc errand run</code> from{" "}
<code style={mono}>@logicsrc/openerrand</code>, is in progress. The worked example
Status: 0.1. The reference runner is <code style={mono}>logicsrc errand run</code>, from{" "}
<code style={mono}>@logicsrc/openerrand</code>: it reads an errand file and drives
headless Chrome through it under the rules below. The worked example
transcribes the rule table of <code style={mono}>ftb</code> in cli-tools, which registers
and activates MyFTB accounts at the California Franchise Tax Board. An excerpt:
</p>
Expand Down
22 changes: 18 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ social
email
credentials
fleet
errand
openspec
plugins
tui
Expand Down
18 changes: 17 additions & 1 deletion docs/openerrand.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,23 @@ npx @logicsrc/validators openerrand ftb-register-business.json

## Reference runner

In progress: a generic runner, the `@logicsrc/openerrand` package in the LogicSRC repository, run as `logicsrc errand run <file>`, reads an errand file and drives headless Chrome through it under the rules above. Until it ships, `ftb` in cli-tools is the runner the worked example was taken from; it has the same rule table compiled in rather than reading the file.
`logicsrc errand run <file>`, from the [`@logicsrc/openerrand`](https://github.com/profullstack/logicsrc/tree/master/packages/openerrand) package, reads an errand file, validates it with `@logicsrc/validators`, and drives headless Chrome through it under the rules above. `logicsrc errand validate <file>` shows what a file will ask of you; `logicsrc errand status` shows the last run of each errand, its hand-off card and any lockout.

```
logicsrc errand run ftb-register-business.json --extractor "python3 extract.py ~/taxes" --dry-run
logicsrc errand run ftb-register-business.json --extractor "python3 extract.py ~/taxes" --declare --vault teams:profullstack/ftb/prod
```

What the runner adds that the file does not say:

- **Documents** come from an extractor the principal names with `--extractor`: a local command that reads the requested forms and fields as JSON on stdin and prints records (`form`, `field`, `value`, `year`, `label`, `file`, `page`). The runner ships no extractor of its own.
- **The vault** is `--vault`, or a string in the file's `metadata.vault`: `teams:<team>/<project>/<env>` (read with `logicsrc teams pull`, written by pull, merge, push), `opencreds` (read-only), or `file:<path>`. With none, credentials go to a 0600 file under `~/.local/share/logicsrc/errand/credentials/` and the runner says so.
- **A code** is typed at the prompt, or written to `~/.local/share/logicsrc/errand/codes/<name>.code` by whoever holds the phone. A wrong code waits for the next one.
- **The throttle**: 2 runs of an errand per account in 30 minutes, 4 a day, 2 minutes between any two runs on one site, and nothing at all during a recorded lockout. A lockout is read from `metadata.lockout.text` (a pattern) and lasts `metadata.lockout.duration`, or a default pattern and 35 minutes. `--force` lifts the caps and never a lockout.
- **One Chrome profile per site** is kept between runs, so a bot check the browser has passed stays passed. The user agent drops `HeadlessChrome` and nothing more.
- **A captcha solver** is an interface a program embedding the runner may pass; none is bundled, and the runner calls one only where the [captcha rules](#captcha) permit it.

`ftb` in [cli-tools](https://github.com/profullstack/cli-tools/pull/125) is the runner the worked example was taken from; it has the same rule table compiled in rather than reading the file.

## Not

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
"apps/*"
],
"scripts": {
"build": "bun run --cwd packages/schemas build && bun run --cwd packages/validators build && bun run --cwd packages/sdk build && bun run --cwd packages/agentad build && bun run --cwd packages/ans build && bun run --cwd packages/plugin-core build && bun run --cwd packages/agentstack build && bun run --cwd packages/agentswarm build && bun run --cwd packages/account-core build && bun run --cwd plugins/coinpay build && bun run --cwd plugins/ugig build && bun run --cwd plugins/sh1pt build && bun run --cwd plugins/c0mpute build && bun run --cwd plugins/feed-discovery build && bun run --cwd plugins/social-accounts build && bun run --cwd plugins/email-accounts build && bun run --cwd plugins/agentbbs build && bun run --cwd plugins/agentgit build && bun run --cwd plugins/agentmail build && bun run --cwd plugins/credential-sharing build && bun run --cwd packages/openontology build && bun run --cwd packages/openprd build && bun run --cwd packages/opencontext build && bun run --cwd packages/opencreds build && bun run --cwd packages/tui build && bun run --cwd packages/openfleet build && bun run --cwd packages/cli build && bun run --cwd packages/logicsrc-mcp build && bun run --cwd apps/commandboard-api build && bun run --cwd apps/commandboard-web build && bun run --cwd apps/logicsrc-web build",
"build": "bun run --cwd packages/schemas build && bun run --cwd packages/validators build && bun run --cwd packages/sdk build && bun run --cwd packages/agentad build && bun run --cwd packages/ans build && bun run --cwd packages/plugin-core build && bun run --cwd packages/agentstack build && bun run --cwd packages/agentswarm build && bun run --cwd packages/account-core build && bun run --cwd plugins/coinpay build && bun run --cwd plugins/ugig build && bun run --cwd plugins/sh1pt build && bun run --cwd plugins/c0mpute build && bun run --cwd plugins/feed-discovery build && bun run --cwd plugins/social-accounts build && bun run --cwd plugins/email-accounts build && bun run --cwd plugins/agentbbs build && bun run --cwd plugins/agentgit build && bun run --cwd plugins/agentmail build && bun run --cwd plugins/credential-sharing build && bun run --cwd packages/openontology build && bun run --cwd packages/openprd build && bun run --cwd packages/opencontext build && bun run --cwd packages/opencreds build && bun run --cwd packages/tui build && bun run --cwd packages/openfleet build && bun run --cwd packages/openerrand build && bun run --cwd packages/cli build && bun run --cwd packages/logicsrc-mcp build && bun run --cwd apps/commandboard-api build && bun run --cwd apps/commandboard-web build && bun run --cwd apps/logicsrc-web build",
"start": "bun run --cwd apps/logicsrc-web start",
"test": "bun run --filter '*' test",
"check": "bun run build && bun run test",
"schemas:validate": "bun run --cwd packages/validators validate:fixtures",
"test:contract": "bun run --cwd apps/commandboard-api test:contract && bun run --cwd apps/logicsrc-web test:contract",
"test:e2e": "bun run --cwd apps/commandboard-web test:e2e && bun run --cwd apps/logicsrc-web test:e2e",
"build:cli": "bun run --cwd packages/schemas build && bun run --cwd packages/validators build && bun run --cwd packages/plugin-core build && bun run --cwd packages/account-core build && bun run --cwd plugins/coinpay build && bun run --cwd plugins/ugig build && bun run --cwd plugins/feed-discovery build && bun run --cwd plugins/social-accounts build && bun run --cwd plugins/email-accounts build && bun run --cwd plugins/agentbbs build && bun run --cwd plugins/credential-sharing build && bun run --cwd packages/openontology build && bun run --cwd packages/openprd build && bun run --cwd packages/opencontext build && bun run --cwd packages/opencreds build && bun run --cwd packages/tui build && bun run --cwd packages/logicsrc-mcp build && bun run --cwd packages/openfleet build && bun run --cwd packages/cli build"
"build:cli": "bun run --cwd packages/schemas build && bun run --cwd packages/validators build && bun run --cwd packages/plugin-core build && bun run --cwd packages/account-core build && bun run --cwd plugins/coinpay build && bun run --cwd plugins/ugig build && bun run --cwd plugins/feed-discovery build && bun run --cwd plugins/social-accounts build && bun run --cwd plugins/email-accounts build && bun run --cwd plugins/agentbbs build && bun run --cwd plugins/credential-sharing build && bun run --cwd packages/openontology build && bun run --cwd packages/openprd build && bun run --cwd packages/opencontext build && bun run --cwd packages/opencreds build && bun run --cwd packages/tui build && bun run --cwd packages/logicsrc-mcp build && bun run --cwd packages/openfleet build && bun run --cwd packages/openerrand build && bun run --cwd packages/cli build"
},
"devDependencies": {
"@types/node": "^24.10.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@logicsrc/cli",
"version": "0.6.0",
"version": "0.7.0",
"description": "LogicSRC CLI: every LogicSRC standard and tool as one command.",
"type": "module",
"main": "./dist/index.js",
Expand All @@ -18,6 +18,7 @@
"@logicsrc/account-core": "file:../account-core",
"@logicsrc/opencontext": "file:../opencontext",
"@logicsrc/opencreds": "file:../opencreds",
"@logicsrc/openerrand": "file:../openerrand",
"@logicsrc/openfleet": "file:../openfleet",
"@logicsrc/openmcp": "^0.3.1",
"@logicsrc/openontology": "file:../openontology",
Expand Down
42 changes: 42 additions & 0 deletions packages/cli/src/errand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { Command } from "commander";
import { afterEach, describe, expect, it } from "vitest";
import { registerErrandCommands } from "./errand.js";

/** A program shaped like the real one: positional options on, no process.exit. */
function program(): Command {
const p = new Command();
p.name("logicsrc").enablePositionalOptions().exitOverride();
return p;
}

const example = fileURLToPath(new URL("../../schemas/fixtures/openerrand/ftb-register-business.json", import.meta.url));

describe("logicsrc errand", () => {
const homes: string[] = [];
afterEach(() => {
for (const home of homes.splice(0)) rmSync(home, { recursive: true, force: true });
process.exitCode = 0;
});

it("mounts run, validate and status", () => {
const p = program();
registerErrandCommands(p);
const errand = p.commands.find((c) => c.name() === "errand")!;
expect(errand.commands.map((c) => c.name()).sort()).toEqual(["run", "status", "validate"]);
});

it("validates the spec's worked example through the umbrella", async () => {
const out: string[] = [];
const home = mkdtempSync(join(tmpdir(), "logicsrc-errand-cli-"));
homes.push(home);
const p = program();
registerErrandCommands(p, { out: (l) => out.push(l), say: () => undefined, env: { LOGICSRC_ERRAND_HOME: home } });
await p.parseAsync(["node", "logicsrc", "errand", "validate", example]);
expect(process.exitCode).toBe(0);
expect(out.at(-1)).toMatch(/^valid OpenErrand 0\.1/);
});
});
35 changes: 35 additions & 0 deletions packages/cli/src/errand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { spawnSync } from "node:child_process";
import type { Command } from "commander";
import { registerErrandCommands as registerRunner, type Deps, type LogicsrcExec } from "@logicsrc/openerrand/commands";

/**
* `logicsrc errand …`
*
* The runner lives in `@logicsrc/openerrand`, the reference implementation of
* OpenErrand; this file only mounts it. A team vault named by an errand
* (`teams:<team>/<project>/<env>`) is read and written through this same CLI's
* `teams pull` / `teams push`, re-entered as a child process so the runner
* never needs the CLI's session code and the plaintext lives for one call.
*
* `deps` is injectable so the umbrella test drives the group without a
* terminal, a browser or a vault.
*/
export function registerErrandCommands(program: Command, deps: Partial<Deps> = {}): void {
const errand = program
.command("errand")
.description(
"OpenErrand: run an errand file on a website with no API, in headless Chrome, stopping at every step " +
"that belongs to a person (declarations, identity proofing, codes, letters, captchas).",
);

const self: LogicsrcExec = (args, options) => {
const entry = process.argv[1];
const result = entry
? spawnSync(process.execPath, [entry, ...args], { encoding: "utf8", stdio: [options?.inheritStdin ? "inherit" : "ignore", "pipe", "pipe"] })
: spawnSync("logicsrc", args, { encoding: "utf8", stdio: [options?.inheritStdin ? "inherit" : "ignore", "pipe", "pipe"] });
if (result.error) return { status: 1, stdout: "", stderr: result.error.message };
return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
};

registerRunner(errand, { logicsrc: self, ...deps });
}
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { exportOpenSpecSummary, importOpenSpec, writeOpenSpecChange } from "./op
import { registerOpenContextCommands } from "./context.js";
import { registerOpenCredsCommands } from "./creds.js";
import { registerFleetCommands } from "./fleet.js";
import { registerErrandCommands } from "./errand.js";
import { registerOntologyCommands } from "./ontology.js";
import { registerPrdCommands } from "./prd.js";
import { registerOpenMcpCommands } from "./openmcp.js";
Expand Down Expand Up @@ -1176,6 +1177,7 @@ registerOpenCredsCommands(program);
registerOntologyCommands(program);
registerPrdCommands(program);
registerFleetCommands(program);
registerErrandCommands(program);
registerOpenMcpCommands(program);
registerMcpCommands(program);
// Every other word under `logicsrc openspec` is OpenSpec.dev's own CLI.
Expand Down
2 changes: 1 addition & 1 deletion packages/logicsrc-mcp/src/standards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ describe("MCP: OpenPRD", () => {
it("reports the next free id and the allowed lifecycle moves", async () => {
const client = await connect();
// Asserted against the live prd/ directory, so this advances with every PRD added.
expect(toolText(await client.callTool({ name: "prd_next_id", arguments: {} }))).toBe("0009");
expect(toolText(await client.callTool({ name: "prd_next_id", arguments: {} }))).toBe("0010");

const moves = await client.callTool({ name: "prd_next_statuses", arguments: { ref: "0001" } });
const payload = JSON.parse(toolText(moves)) as { status: string; allowedNext: string[] };
Expand Down
Loading
Loading