mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
Multi-host + Living Notes. Multi-host foundation: shareable whole-topology staged spin-up brings a rig topology up across multiple hosts in stages; VPS product-factory multi-host hardening ships with rig host verbs (add / list / doctor, capped at three; one built-in product-factory-vps posture, three- valued per item); the transport posture is documented (ssh for pane ops, http-bearer for daemon REST, no cross-transport fallback); the For-You feed aggregates activity across every registered host; and rig file moves files across the topology's registered hosts. Living Notes: durable INTENT / PLAN / DELIVERED signal layer at mission and slice altitude with agent authorship + timestamps; cheap composer surfaces make it the first-class place agents record decisions, plans, and delivered work; the one-structure review contract reads left-to-right as a single vertical stack (INTENT above, PLAN + mockup in the middle, DELIVERED with paired proof at the bottom); a plan change deletes the old plan and writes a new one - never demotes or stacks multiple competing plans. Operationalize the SDLC control plane: conventions SSOT ships in source at docs/reference/sdlc-conventions.md (Living Notes UI section names, proof-contract format + plannedRef mockup pairing, staged- approval locks, C1 proof header + closed sets, three role contracts, curation rule, elastic-middle doctrine, advisory fail-open audit posture); rig scope slice create scaffolds the convention sections + proof + PROOF.md + an IMPLEMENTATION-PRD.md skeleton for every template kind; rig scope audit + rig workspace doctor gain sdlc- convention advisories (records-and-advises, gates on HIGH only); the mission-slice-sop skill ships in canonical source with byte-parity guard; rig proof / rig scope slice create / rig scope slice approve help + cli-reference teach the SDLC flow. BREAKING: rig ps default flips to consolidated all-active-rigs compact projection (the v0.4.0 current-rig-only default is retired - it hid running rigs from the operator's field of view). --json is scope-not-shape (still a bare array with existing per-entry keys; scope widens to all non-archived rigs including stopped ones; one-line migration for the old fleet firehose: rig ps --nodes -A --full). -A / --all-rigs keeps exactly ONE meaning (the --nodes fleet widener; bare -A is a structured teaching error). --nodes names its scope everywhere (session default local-only; --host <id> --nodes requires --rig or -A; multi-host fan-out is rollup-only by default; the explicit ladder --all-hosts --nodes -A --full fans out per-node with hostId-stamped projected rows). --all-hosts / --hosts --json emits the shared AggregatedPayload (items + hosts with closed-enum statuses: ok | unreachable | unsupported-transport | auth-failed). Operator UX: agent altitude coordination panel scopes multi-agent state to the right altitude (workspace / mission / slice) so the coordination surface reads without per-seat drowning; composes with the workspace observability tabs from 0.4.1. Docs closeout catches docs/as-built/architecture.md, docs/as-built/cli-reference.md, and the codemaps up to shipped state. Migrations: additive only. Existing v0.4.3 databases upgrade by running rig daemon start. Behavior change (rig ps default view): the compact all-active-rigs projection replaces the current-rig-only default. If your call sites relied on the v0.4.3 default shape, add --full for the rich per-node projection or --rig <name> for the prior scope. Public-safety scrub applied inline to packages/ui/twin/corrective/ fixtures-corrective.ts (one demo-fixture role identifier neutralized to the demo-namespace baseline established at 0.4.1). See CHANGELOG.md and docs/releases/v0.4.4.md.
259 lines
10 KiB
TypeScript
259 lines
10 KiB
TypeScript
// OPR.0.4.4.19 FR-8 + FR-11 — rig proof add: C1 header validation at drop
|
|
// time, D2 attestation echo, contract + C8 advisories (advise-never-block).
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import YAML from "yaml";
|
|
import {
|
|
proofCommand,
|
|
validateC1Header,
|
|
parseProofContract,
|
|
C1_ARTIFACT_TYPES,
|
|
C1_VERDICTS,
|
|
} from "../src/commands/proof.js";
|
|
|
|
describe("validateC1Header (pure)", () => {
|
|
const valid = {
|
|
slice: "OPR.0.4.4.19",
|
|
candidate_sha: "abc1234",
|
|
artifact_type: "qa",
|
|
verdict: "CLEAR",
|
|
money_evidence: "park->resolve walk transitions row shows decision text",
|
|
};
|
|
|
|
it("accepts the five required fields with closed-set values", () => {
|
|
expect(validateC1Header(valid).ok).toBe(true);
|
|
});
|
|
|
|
it("names every missing field", () => {
|
|
const r = validateC1Header({ artifact_type: "qa" });
|
|
expect(r.ok).toBe(false);
|
|
expect(r.missing).toEqual(["slice", "candidate_sha", "verdict", "money_evidence"]);
|
|
});
|
|
|
|
it("rejects out-of-set artifact_type/verdict naming the allowed values (BR-4 closed sets)", () => {
|
|
const r = validateC1Header({ ...valid, artifact_type: "designer", verdict: "SHIP-IT" });
|
|
expect(r.ok).toBe(false);
|
|
expect(r.invalid).toHaveLength(2);
|
|
expect(r.invalid[0]!.allowed).toEqual(C1_ARTIFACT_TYPES);
|
|
expect(r.invalid[1]!.allowed).toEqual(C1_VERDICTS);
|
|
});
|
|
});
|
|
|
|
describe("parseProofContract (pure)", () => {
|
|
it("returns null when no ## Proof contract section exists (zero-noise degrade)", () => {
|
|
expect(parseProofContract("# PRD\n\n## Acceptance\n- [ ] thing\n")).toBeNull();
|
|
});
|
|
|
|
it("parses checkbox items until the next section", () => {
|
|
const prd = [
|
|
"# PRD",
|
|
"## Proof contract",
|
|
"- [ ] the live park->resolve walk with the transitions row shown",
|
|
"- [x] approve run showing frontmatter + audit row together",
|
|
"- plain item without checkbox",
|
|
"## Next section",
|
|
"- [ ] NOT a contract item",
|
|
].join("\n");
|
|
expect(parseProofContract(prd)).toEqual([
|
|
"the live park->resolve walk with the transitions row shown",
|
|
"approve run showing frontmatter + audit row together",
|
|
"plain item without checkbox",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("rig proof add (fs-level, temp workspace)", () => {
|
|
let workRoot: string;
|
|
let sliceDir: string;
|
|
let logs: string[];
|
|
let errs: string[];
|
|
|
|
beforeEach(() => {
|
|
workRoot = fs.mkdtempSync(path.join(os.tmpdir(), "proof-test-"));
|
|
const missionDir = path.join(workRoot, "missions", "release-x", "slices", "19-signal-layer");
|
|
fs.mkdirSync(missionDir, { recursive: true });
|
|
fs.writeFileSync(path.join(workRoot, "missions", "release-x", "README.md"), "---\nid: OPR.X\n---\n# m\n");
|
|
sliceDir = missionDir;
|
|
fs.writeFileSync(
|
|
path.join(sliceDir, "README.md"),
|
|
"---\nid: OPR.X.19\nstatus: building\n---\n# slice\n",
|
|
);
|
|
logs = [];
|
|
errs = [];
|
|
vi.spyOn(console, "log").mockImplementation((...a: unknown[]) => { logs.push(a.join(" ")); });
|
|
vi.spyOn(console, "error").mockImplementation((...a: unknown[]) => { errs.push(a.join(" ")); });
|
|
process.exitCode = undefined;
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
process.exitCode = undefined;
|
|
});
|
|
|
|
async function run(args: string[]): Promise<void> {
|
|
const cmd = proofCommand();
|
|
cmd.exitOverride();
|
|
await cmd.parseAsync(["node", "proof", "--workspace", workRoot, ...args]);
|
|
}
|
|
|
|
const baseArgs = (extra: string[] = []) => [
|
|
"add", "19-signal-layer", "--mission", "release-x",
|
|
"--artifact-type", "qa", "--verdict", "CLEAR",
|
|
"--candidate-sha", "abc1234",
|
|
"--money-evidence", "one line of money",
|
|
"--body", "evidence body",
|
|
"--name", "qa-clear.md",
|
|
...extra,
|
|
];
|
|
|
|
it("happy drop: writes proof/<name> with valid YAML frontmatter and echoes the parsed header; exit 0", async () => {
|
|
await run(baseArgs());
|
|
const target = path.join(sliceDir, "proof", "qa-clear.md");
|
|
expect(fs.existsSync(target)).toBe(true);
|
|
const content = fs.readFileSync(target, "utf8");
|
|
const fm = content.split("---")[1]!;
|
|
const parsed = YAML.parse(fm) as Record<string, unknown>;
|
|
expect(parsed.slice).toBe("OPR.X.19");
|
|
expect(parsed.candidate_sha).toBe("abc1234");
|
|
expect(parsed.artifact_type).toBe("qa");
|
|
expect(parsed.verdict).toBe("CLEAR");
|
|
expect(content).toContain("evidence body");
|
|
expect(logs.join("\n")).toContain("Parsed C1 header");
|
|
expect(process.exitCode).toBeUndefined();
|
|
});
|
|
|
|
it("out-of-set verdict is rejected naming the allowed values; nothing written; exit 1", async () => {
|
|
await run([
|
|
"add", "19-signal-layer", "--mission", "release-x",
|
|
"--artifact-type", "qa", "--verdict", "SHIP-IT",
|
|
"--candidate-sha", "abc1234", "--money-evidence", "m", "--body", "b",
|
|
]);
|
|
expect(process.exitCode).toBe(1);
|
|
expect(errs.join("\n")).toContain("closed set");
|
|
expect(fs.existsSync(path.join(sliceDir, "proof"))).toBe(false);
|
|
});
|
|
|
|
it("D2: evidences + self_check are parsed and echoed; unknown refs warn (never reject)", async () => {
|
|
fs.writeFileSync(
|
|
path.join(sliceDir, "IMPLEMENTATION-PRD.md"),
|
|
"# PRD\n## Proof contract\n- [ ] item one\n- [ ] item two\n",
|
|
);
|
|
await run(baseArgs(["--evidences", "1,bogus-ref", "--self-check", "I looked; the walk shows the decision text"]));
|
|
expect(process.exitCode).toBeUndefined();
|
|
const content = fs.readFileSync(path.join(sliceDir, "proof", "qa-clear.md"), "utf8");
|
|
expect(content).toContain("self_check");
|
|
expect(errs.join("\n")).toContain("bogus-ref");
|
|
});
|
|
|
|
it("contract advisory: declared contract + no covered item/self_check => drop SUCCEEDS with advisory naming uncovered items", async () => {
|
|
fs.writeFileSync(
|
|
path.join(sliceDir, "IMPLEMENTATION-PRD.md"),
|
|
"# PRD\n## Proof contract\n- [ ] item one\n",
|
|
);
|
|
await run(baseArgs());
|
|
expect(process.exitCode).toBeUndefined();
|
|
expect(fs.existsSync(path.join(sliceDir, "proof", "qa-clear.md"))).toBe(true);
|
|
expect(errs.join("\n")).toContain("ADVISORY (D2");
|
|
expect(errs.join("\n")).toContain("item one");
|
|
});
|
|
|
|
it("zero noise: NO contract declared => no contract advisory", async () => {
|
|
await run(baseArgs());
|
|
expect(errs.join("\n")).not.toContain("ADVISORY (D2");
|
|
});
|
|
|
|
it("C8: ux-change slice + no video => drop succeeds with the screencast advisory; exit 0", async () => {
|
|
fs.writeFileSync(
|
|
path.join(sliceDir, "README.md"),
|
|
"---\nid: OPR.X.19\nstatus: building\nux-change: true\n---\n# slice\n",
|
|
);
|
|
await run(baseArgs());
|
|
expect(process.exitCode).toBeUndefined();
|
|
expect(errs.join("\n")).toContain("ADVISORY (C8");
|
|
expect(errs.join("\n")).toContain("agent-browser-screencast");
|
|
});
|
|
|
|
it("C8 zero noise: no ux-change flag => no video advisory; existing video also silences it", async () => {
|
|
await run(baseArgs());
|
|
expect(errs.join("\n")).not.toContain("ADVISORY (C8");
|
|
// Now flag the slice AND plant a video — advisory stays silent.
|
|
fs.writeFileSync(
|
|
path.join(sliceDir, "README.md"),
|
|
"---\nid: OPR.X.19\nux-change: true\n---\n# slice\n",
|
|
);
|
|
fs.writeFileSync(path.join(sliceDir, "proof", "walk.mp4"), "fake video bytes");
|
|
errs.length = 0;
|
|
await run(baseArgs(["--name", "qa-clear-2.md"]));
|
|
expect(errs.join("\n")).not.toContain("ADVISORY (C8");
|
|
});
|
|
});
|
|
|
|
// rev1-r2 BLOCKING regression (candidate a7dedd93 review): --name must be a
|
|
// filename, never a path — a traversal name can never escape proof/.
|
|
describe("rig proof add --name traversal rejection (rev1-r2 fixback)", () => {
|
|
let workRoot: string;
|
|
let sliceDir: string;
|
|
let errs: string[];
|
|
|
|
beforeEach(() => {
|
|
workRoot = fs.mkdtempSync(path.join(os.tmpdir(), "proof-trav-"));
|
|
sliceDir = path.join(workRoot, "missions", "release-x", "slices", "19-signal-layer");
|
|
fs.mkdirSync(sliceDir, { recursive: true });
|
|
fs.writeFileSync(path.join(workRoot, "missions", "release-x", "README.md"), "---\nid: OPR.X\n---\n# m\n");
|
|
fs.writeFileSync(path.join(sliceDir, "README.md"), "---\nid: OPR.X.19\nstatus: building\n---\n# ORIGINAL README BODY\n");
|
|
errs = [];
|
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
vi.spyOn(console, "error").mockImplementation((...a: unknown[]) => { errs.push(a.join(" ")); });
|
|
process.exitCode = undefined;
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
process.exitCode = undefined;
|
|
});
|
|
|
|
async function runAdd(name: string): Promise<void> {
|
|
const cmd = proofCommand();
|
|
cmd.exitOverride();
|
|
await cmd.parseAsync([
|
|
"node", "proof", "--workspace", workRoot,
|
|
"add", "19-signal-layer", "--mission", "release-x",
|
|
"--artifact-type", "qa", "--verdict", "CLEAR",
|
|
"--candidate-sha", "abc1234", "--money-evidence", "m",
|
|
"--body", "traversal attempt body",
|
|
"--name", name,
|
|
]);
|
|
}
|
|
|
|
it("--name ../README.md is REJECTED and the slice README is NOT modified", async () => {
|
|
const readmePath = path.join(sliceDir, "README.md");
|
|
const before = fs.readFileSync(readmePath, "utf8");
|
|
await runAdd("../README.md");
|
|
expect(process.exitCode).toBe(1);
|
|
expect(errs.join("\n")).toContain("not a plain filename");
|
|
expect(fs.readFileSync(readmePath, "utf8")).toBe(before);
|
|
// Nothing landed in proof/ either.
|
|
const proofDir = path.join(sliceDir, "proof");
|
|
expect(!fs.existsSync(proofDir) || fs.readdirSync(proofDir).length === 0).toBe(true);
|
|
});
|
|
|
|
it("other escape shapes are rejected: nested path, backslash, absolute, dot-dot", async () => {
|
|
for (const name of ["sub/dir.md", "..\\\\evil.md", "/tmp/abs.md", ".."]) {
|
|
process.exitCode = undefined;
|
|
await runAdd(name);
|
|
expect(process.exitCode, `name '${name}' must be rejected`).toBe(1);
|
|
}
|
|
});
|
|
|
|
it("a plain filename still drops normally after the fix", async () => {
|
|
await runAdd("qa-clear.md");
|
|
expect(process.exitCode).toBeUndefined();
|
|
expect(fs.existsSync(path.join(sliceDir, "proof", "qa-clear.md"))).toBe(true);
|
|
});
|
|
});
|