mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 13:56:40 -10:00
feat(0.3.2 slice 01): workspace + workflow GA coherence (HG-1..HG-4)
Release-0.3.2 slice 01 (OPR.0.3.2.1) — GA polish + scope-and-versioning
coherence pass on already-shipped primitives.
HG-3 — init-workspace scaffold mints §1 dot-IDs into seeded
mission + slice frontmatter, satisfying the convention without
requiring operators to round-trip through rig scope:
- packages/daemon/src/domain/workspace/default-workspace-scaffold.ts
- packages/cli/src/commands/config-init-workspace.ts (parity mirror)
Mission `getting-started` → id: OPR.99.0.1 (escape-band, per §1's
uniform-numeric rule for non-release missions). Slices
`first-conveyor-run` → OPR.99.0.1.1, `inspect-project-evidence` →
OPR.99.0.1.2 (monotonic, never-reused). Folder names stay
descriptive ("names are prompts; IDs are the stable handle"); the
id: lives in frontmatter where the convention places it.
Both scaffold files emit identical body output — the
getting-started-narrative-parity test (slice 21) still passes.
README copy updated on both sides to reference the convention +
`rig scope mission/slice create` for new artifacts (replaces stale
"use stable kebab-case ids" guidance).
HG-4 — rig workflow UX polish:
- packages/cli/src/commands/workflow.ts:
* Added .addHelpText('after', ...) with concrete `$ rig workflow ...`
example invocations to ALL 8 verbs (validate, instantiate,
project, list, specs, show, trace, continue).
* Exported printOutcomeSummary helper + OutcomeSummary type;
every mutating verb (instantiate, project, continue) now ends
human-mode output with a 3-line `what / state / next` summary
after the JSON body. Summary auto-suppresses in --json mode
and on error (status >= 400), so machine consumers see clean
JSON and humans get a glanceable outcome.
* instantiate summary names the new instance + entry packet +
entry owner; project names the closed packet + projected next +
instance status; continue distinguishes "advanced" vs "no-op
at frontier" cases.
- packages/cli/test/workflow-ux.test.ts (new, 14 tests):
* source-scan every verb for .addHelpText("after", ...) carrying
an Examples section + at least one `$ rig workflow ...`
invocation (commander's helpInformation() doesn't render the
after-text, so the test reads the source directly — catches
drift if a future refactor drops addHelpText from one verb)
* printOutcomeSummary contract: 4-line (blank + what + state +
next) in human mode; suppressed in --json mode; suppressed on
error; suppressed when summary is null
HG-1 — coherence audit doc landed at
missions/release-0.3.2/slices/01-workspace-workflow-ga/coherence-audit-2026-05-16.md
(substrate). Enumerates every workspace/workflow surface touching
the mission/slice tree, marks each coherent or fixed against
scope-and-versioning §1/§3.
HG-2 — grep across packages/{cli,daemon}/src returns 0 references to
the retired `rigs/<rig>/scopes/` location. Documented in the audit.
Verification (per-commit checkpoint):
- packages/daemon npx tsc --noEmit: clean
- packages/cli npx tsc --noEmit: clean
- packages/daemon test/getting-started-narrative-parity.test.ts: 9/9 PASS
- packages/cli test/workflow-ux.test.ts: 14/14 PASS
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
7eaf524cc4
commit
a28f9b710e
4 files changed
+308
-7
No files matched your search
@@ -39,6 +39,11 @@ export interface InitWorkspaceResult {
|
||||
|
||||
type DefaultSlice = {
|
||||
id: string;
|
||||
/** §1 dot-ID per conventions/scope-and-versioning (release-0.3.2
|
||||
* slice 01 GA-coherence pass). Mirror of the daemon constant in
|
||||
* packages/daemon/src/domain/workspace/default-workspace-scaffold.ts
|
||||
* — keep in lockstep for the slice-21 parity test. */
|
||||
dotId: string;
|
||||
title: string;
|
||||
status: "active" | "draft";
|
||||
objective: string;
|
||||
@@ -46,27 +51,36 @@ type DefaultSlice = {
|
||||
|
||||
type DefaultMission = {
|
||||
id: string;
|
||||
/** §1 mission dot-ID — escape band for non-release missions. */
|
||||
dotId: string;
|
||||
title: string;
|
||||
status: "active" | "draft";
|
||||
objective: string;
|
||||
slices: DefaultSlice[];
|
||||
};
|
||||
|
||||
// Release-0.3.2 slice 01 (OPR.0.3.2.1) — seeded missions/slices now
|
||||
// carry §1 dot-IDs in frontmatter so the scaffold coheres with
|
||||
// `conventions/scope-and-versioning`. Mirror of the daemon constant;
|
||||
// the slice-21 parity test asserts both sides emit identical bodies.
|
||||
const DEFAULT_MISSIONS: DefaultMission[] = [
|
||||
{
|
||||
id: "getting-started",
|
||||
dotId: "OPR.99.0.1",
|
||||
title: "Getting Started",
|
||||
status: "active",
|
||||
objective: "Launch the conveyor starter and learn how OpenRig moves work through queue-backed slices.",
|
||||
slices: [
|
||||
{
|
||||
id: "first-conveyor-run",
|
||||
dotId: "OPR.99.0.1.1",
|
||||
title: "First Conveyor Run",
|
||||
status: "active",
|
||||
objective: "Move one small packet through intake, planning, build, review, and close on the conveyor starter.",
|
||||
},
|
||||
{
|
||||
id: "inspect-project-evidence",
|
||||
dotId: "OPR.99.0.1.2",
|
||||
title: "Inspect Project Evidence",
|
||||
status: "draft",
|
||||
objective: "Open Project, Queue, Story, and Tests to inspect the evidence created by the first conveyor run.",
|
||||
@@ -93,7 +107,7 @@ const WORKSPACE_DIRS = [
|
||||
function subdirReadmeContent(subdir: string): string {
|
||||
switch (subdir) {
|
||||
case "missions":
|
||||
return "# missions\n\nProject missions live here. Each mission folder maps to one Project mission in the UI and owns a `slices/` child folder.\n\nExpected shape:\n\n```text\nmissions/<mission-id>/README.md\nmissions/<mission-id>/PROGRESS.md\nmissions/<mission-id>/slices/<slice-id>/README.md\n```\n";
|
||||
return "# missions\n\nProject missions live here. Each mission folder maps to one Project mission in the UI and owns a `slices/` child folder.\n\nExpected shape:\n\n```text\nmissions/<mission-name>/README.md\nmissions/<mission-name>/PROGRESS.md\nmissions/<mission-name>/slices/<slice-name>/README.md\n```\n\nEvery mission and slice README carries a stable dot-ID in its frontmatter (`id: OPR.<ver>[.<n>]`) per the scope-and-versioning convention. Use `rig scope mission create <name>` and `rig scope slice create <mission> <slug>` to mint conformant artifacts; the CLI handles the dot-ID + auto-numbering for you. The folder name is the operator-facing slug; the `id:` is the stable handle that survives renames.\n";
|
||||
case "artifacts":
|
||||
return "# artifacts\n\nWork products live here: plans, drafts, generated outputs, and other files that a slice may reference before closure.\n";
|
||||
case "evidence":
|
||||
@@ -122,7 +136,7 @@ This workspace is file-backed. The Project UI mirrors this structure:
|
||||
- \`evidence/\` is for proof notes and verification summaries.
|
||||
- \`dogfood-evidence/<proof-packet-id>\` becomes Tests proof when the packet id contains the slice id tokens.
|
||||
|
||||
Use stable kebab-case ids for mission and slice folders. Keep slice ids unique inside the workspace.
|
||||
Use stable kebab-case names for mission and slice folders. Every mission/slice README also carries a stable dot-ID in its frontmatter (\`id: OPR.<ver>[.<n>]\`) per the \`scope-and-versioning\` convention; mint conformant artifacts via \`rig scope mission create <name>\` and \`rig scope slice create <mission> <slug>\`. Folder names are operator-facing slugs; the \`id:\` is the rename-proof handle.
|
||||
`;
|
||||
|
||||
const STEERING_PLACEHOLDER = `---
|
||||
@@ -153,6 +167,7 @@ alongside your Project workspace.
|
||||
|
||||
function missionReadme(mission: DefaultMission): string {
|
||||
return `---
|
||||
id: ${mission.dotId}
|
||||
title: ${mission.title}
|
||||
status: ${mission.status}
|
||||
mission: ${mission.id}
|
||||
@@ -423,6 +438,7 @@ function sliceReadme(mission: DefaultMission, slice: DefaultSlice): string {
|
||||
const narrative = GETTING_STARTED_NARRATIVE[slice.id];
|
||||
if (narrative) {
|
||||
return `---
|
||||
id: ${slice.dotId}
|
||||
title: ${slice.title}
|
||||
status: ${slice.status}
|
||||
mission: ${mission.id}
|
||||
@@ -433,6 +449,7 @@ slice: ${slice.id}
|
||||
${narrative.readme}`;
|
||||
}
|
||||
return `---
|
||||
id: ${slice.dotId}
|
||||
title: ${slice.title}
|
||||
status: ${slice.status}
|
||||
mission: ${mission.id}
|
||||
|
||||
@@ -41,6 +41,29 @@ function printResult(json: boolean, body: unknown, status: number): void {
|
||||
if (status >= 400) process.exitCode = status >= 500 ? 2 : 1;
|
||||
}
|
||||
|
||||
// release-0.3.2 slice 01 GA polish — every mutating workflow command
|
||||
// ends with a 3-line what/state/next summary in human mode so the
|
||||
// operator doesn't have to grep the raw JSON for the outcome. JSON
|
||||
// mode keeps the daemon response verbatim — agent consumers parse
|
||||
// the structured body, not the human summary.
|
||||
export interface OutcomeSummary {
|
||||
what: string;
|
||||
state: string;
|
||||
next: string;
|
||||
}
|
||||
|
||||
export function printOutcomeSummary(json: boolean, status: number, summary: OutcomeSummary | null): void {
|
||||
if (json || !summary || status >= 400) return;
|
||||
console.log("");
|
||||
console.log(` what: ${summary.what}`);
|
||||
console.log(` state: ${summary.state}`);
|
||||
console.log(` next: ${summary.next}`);
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
const cmd = new Command("workflow").description(
|
||||
"Daemon-native Workflow Runtime — declarative spec + transactional-scribe step projection (PL-004 Phase D)",
|
||||
@@ -55,6 +78,11 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
.command("validate <specPath>")
|
||||
.description("Validate a workflow spec file (returns structured ok/error report)")
|
||||
.option("--json", "JSON output for agents")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ rig workflow validate workflows/conveyor-starter.workflow.md
|
||||
$ rig workflow validate ./my-spec.workflow.md --json | jq .ok
|
||||
`)
|
||||
.action(async (specPath: string, opts: { json?: boolean }) => {
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
@@ -70,6 +98,17 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
.requiredOption("--created-by <session>", "Session creating the instance (canonical <member>@<rig>)")
|
||||
.option("--entry-owner <session>", "Override default entry-step owner")
|
||||
.option("--json", "JSON output for agents")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ rig workflow instantiate workflows/conveyor.workflow.md \\
|
||||
--root-objective "Ship release-0.3.2" \\
|
||||
--created-by orch-lead@openrig-velocity
|
||||
|
||||
$ rig workflow instantiate ./my-spec.workflow.md \\
|
||||
--root-objective "Run dogfood" \\
|
||||
--created-by velocity-driver@openrig-velocity \\
|
||||
--entry-owner velocity-qa@openrig-velocity --json
|
||||
`)
|
||||
.action(async (specPath: string, opts: {
|
||||
rootObjective: string;
|
||||
createdBy: string;
|
||||
@@ -78,13 +117,29 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
}) => {
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>("/api/workflow/instantiate", {
|
||||
const res = await client.post<{
|
||||
instanceId?: string;
|
||||
entryStepId?: string;
|
||||
entryOwnerSession?: string;
|
||||
status?: string;
|
||||
}>("/api/workflow/instantiate", {
|
||||
specPath,
|
||||
rootObjective: opts.rootObjective,
|
||||
createdBySession: opts.createdBy,
|
||||
entryOwnerSession: opts.entryOwner,
|
||||
});
|
||||
printResult(opts.json ?? false, res.data, res.status);
|
||||
const body = res.data ?? {};
|
||||
const instanceId = asString(body.instanceId) ?? "(no instance id)";
|
||||
const entryStepId = asString(body.entryStepId);
|
||||
const owner = asString(body.entryOwnerSession) ?? opts.entryOwner ?? "(default from spec)";
|
||||
printOutcomeSummary(opts.json ?? false, res.status, {
|
||||
what: `Instantiated workflow from ${specPath} (instance ${instanceId})`,
|
||||
state: `${body.status ?? "active"}; entry packet ${entryStepId ?? "pending"} owned by ${owner}`,
|
||||
next: entryStepId
|
||||
? `Inspect: rig workflow show ${instanceId} | Open packet: rig queue show ${entryStepId}`
|
||||
: `Inspect: rig workflow show ${instanceId}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,6 +154,25 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
.option("--blocked-on <ref>", "For waiting exits: blocker reference (qitem id, gate name)")
|
||||
.option("--next-owner <session>", "Override default next-step owner")
|
||||
.option("--json", "JSON output for agents")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
# handoff to the spec's default next step
|
||||
$ rig workflow project \\
|
||||
--instance WF01ABC \\
|
||||
--current-packet QITEM-123 \\
|
||||
--exit handoff \\
|
||||
--actor-session velocity-driver@openrig-velocity \\
|
||||
--result-note "implementation green; ready for review"
|
||||
|
||||
# close the run cleanly
|
||||
$ rig workflow project --instance WF01ABC --current-packet QITEM-9 \\
|
||||
--exit done --actor-session orch-lead@openrig-velocity
|
||||
|
||||
# block on an external gate
|
||||
$ rig workflow project --instance WF01ABC --current-packet QITEM-4 \\
|
||||
--exit waiting --actor-session velocity-qa@openrig-velocity \\
|
||||
--blocked-on "founder-gate-2"
|
||||
`)
|
||||
.action(async (opts: {
|
||||
instance: string;
|
||||
currentPacket: string;
|
||||
@@ -111,7 +185,12 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
}) => {
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>("/api/workflow/project", {
|
||||
const res = await client.post<{
|
||||
closedPacketId?: string;
|
||||
nextPacketId?: string;
|
||||
nextOwnerSession?: string;
|
||||
instanceStatus?: string;
|
||||
}>("/api/workflow/project", {
|
||||
instanceId: opts.instance,
|
||||
currentPacketId: opts.currentPacket,
|
||||
exit: opts.exit,
|
||||
@@ -121,6 +200,22 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
nextOwnerSession: opts.nextOwner,
|
||||
});
|
||||
printResult(opts.json ?? false, res.data, res.status);
|
||||
const body = res.data ?? {};
|
||||
const closedId = asString(body.closedPacketId) ?? opts.currentPacket;
|
||||
const nextId = asString(body.nextPacketId);
|
||||
const nextOwner = asString(body.nextOwnerSession) ?? opts.nextOwner ?? "(default from spec)";
|
||||
const status = body.instanceStatus ?? (opts.exit === "done" ? "completed" : opts.exit === "failed" ? "failed" : "active");
|
||||
const whatTail = nextId ? ` and projected ${nextId} to ${nextOwner}` : (opts.exit === "done" ? " (instance done)" : "");
|
||||
const nextAction = nextId
|
||||
? `Inspect: rig queue show ${nextId}`
|
||||
: (opts.exit === "done" || opts.exit === "failed")
|
||||
? `Inspect: rig workflow show ${opts.instance}`
|
||||
: `Wait for upstream; rig workflow show ${opts.instance} to monitor`;
|
||||
printOutcomeSummary(opts.json ?? false, res.status, {
|
||||
what: `Closed ${closedId} (${opts.exit})${whatTail}`,
|
||||
state: `instance ${opts.instance} = ${status}`,
|
||||
next: nextAction,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,6 +224,12 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
.description("List workflow instances; optionally filter by status")
|
||||
.option("--status <s>", "Filter by status (active | waiting | completed | failed)")
|
||||
.option("--json", "JSON output for agents")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ rig workflow list
|
||||
$ rig workflow list --status active --json
|
||||
$ rig workflow list --status waiting | jq -r '.instances[].instanceId'
|
||||
`)
|
||||
.action(async (opts: { status?: string; json?: boolean }) => {
|
||||
const deps = getDeps();
|
||||
const params = new URLSearchParams();
|
||||
@@ -150,6 +251,11 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
.command("specs")
|
||||
.description("List registered workflow specs; built-in starters tagged with (built-in)")
|
||||
.option("--json", "JSON output for agents")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ rig workflow specs
|
||||
$ rig workflow specs --json | jq '.specs[] | select(.isBuiltIn==false)'
|
||||
`)
|
||||
.action(async (opts: { json?: boolean }) => {
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
@@ -185,6 +291,11 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
.command("show <instanceId>")
|
||||
.description("Show one workflow instance")
|
||||
.option("--json", "JSON output for agents")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ rig workflow show WF01ABC
|
||||
$ rig workflow show WF01ABC --json | jq '.instance.status'
|
||||
`)
|
||||
.action(async (instanceId: string, opts: { json?: boolean }) => {
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
@@ -197,6 +308,11 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
.command("trace <instanceId>")
|
||||
.description("Show one workflow instance + its append-only step trail (audit-only verdict)")
|
||||
.option("--json", "JSON output for agents")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ rig workflow trace WF01ABC
|
||||
$ rig workflow trace WF01ABC --json | jq '.trail[] | {step, actor, exit}'
|
||||
`)
|
||||
.action(async (instanceId: string, opts: { json?: boolean }) => {
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
@@ -209,11 +325,33 @@ export function workflowCommand(depsOverride?: WorkflowDeps): Command {
|
||||
.command("continue <instanceId>")
|
||||
.description("Mechanically advance an instance after truthful closure (idempotent inspector in v1)")
|
||||
.option("--json", "JSON output for agents")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ rig workflow continue WF01ABC
|
||||
$ rig workflow continue WF01ABC --json
|
||||
`)
|
||||
.action(async (instanceId: string, opts: { json?: boolean }) => {
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>(`/api/workflow/${encodeURIComponent(instanceId)}/continue`, {});
|
||||
const res = await client.post<{
|
||||
advanced?: boolean;
|
||||
nextPacketId?: string;
|
||||
nextOwnerSession?: string;
|
||||
instanceStatus?: string;
|
||||
}>(`/api/workflow/${encodeURIComponent(instanceId)}/continue`, {});
|
||||
printResult(opts.json ?? false, res.data, res.status);
|
||||
const body = res.data ?? {};
|
||||
const advanced = body.advanced !== false;
|
||||
const nextId = asString(body.nextPacketId);
|
||||
const nextOwner = asString(body.nextOwnerSession) ?? "(unknown)";
|
||||
const status = body.instanceStatus ?? "(unknown)";
|
||||
printOutcomeSummary(opts.json ?? false, res.status, {
|
||||
what: advanced
|
||||
? `Advanced instance ${instanceId}${nextId ? ` to packet ${nextId}` : ""}`
|
||||
: `No advance for instance ${instanceId} (already at frontier or terminal)`,
|
||||
state: `instance status = ${status}${nextId ? `; next owner = ${nextOwner}` : ""}`,
|
||||
next: nextId ? `Inspect: rig queue show ${nextId}` : `Inspect: rig workflow trace ${instanceId}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// release-0.3.2 slice 01 (OPR.0.3.2.1) — GA-polish coverage for the
|
||||
// rig workflow command surface.
|
||||
//
|
||||
// Scope: static --help + description coverage; the what/state/next
|
||||
// summary behavior is verified at the building-block level via
|
||||
// printOutcomeSummary unit tests (exported for this purpose), since
|
||||
// the live action path is gated by getDaemonStatus and tested at the
|
||||
// integration tier elsewhere.
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import {
|
||||
workflowCommand,
|
||||
printOutcomeSummary,
|
||||
type OutcomeSummary,
|
||||
} from "../src/commands/workflow.js";
|
||||
|
||||
describe("rig workflow --help carries examples on every command (HG-4)", () => {
|
||||
const expectedVerbs = [
|
||||
"validate",
|
||||
"instantiate",
|
||||
"project",
|
||||
"list",
|
||||
"specs",
|
||||
"show",
|
||||
"trace",
|
||||
"continue",
|
||||
];
|
||||
|
||||
// Commander v13 attaches `addHelpText('after', ...)` as a listener
|
||||
// that fires when --help is rendered; `helpInformation()` returns
|
||||
// Usage+Options+Subcommands but NOT the after-text. Source-scan the
|
||||
// command file to prove every verb carries an Examples block —
|
||||
// catches drift if a future refactor drops `addHelpText` from one
|
||||
// verb but leaves the others.
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const workflowSrc = readFileSync(
|
||||
path.resolve(here, "../src/commands/workflow.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
for (const verb of expectedVerbs) {
|
||||
it(`${verb} has an addHelpText('after', ...) block with an Examples section`, () => {
|
||||
const root = workflowCommand();
|
||||
const sub = root.commands.find((c) => c.name() === verb);
|
||||
expect(sub, `verb ${verb} should be registered`).toBeTruthy();
|
||||
// Source-scan: locate the command(...) declaration and look
|
||||
// for an Examples block in the addHelpText that follows.
|
||||
const verbBlockRe = new RegExp(
|
||||
`\\.command\\("${verb}[ "(<]([\\s\\S]*?)\\.action\\(`,
|
||||
"m",
|
||||
);
|
||||
const match = verbBlockRe.exec(workflowSrc);
|
||||
expect(match, `couldn't locate builder for "${verb}"`).toBeTruthy();
|
||||
const block = match![0];
|
||||
expect(block).toMatch(/\.addHelpText\("after",/);
|
||||
expect(block).toMatch(/Examples?:/i);
|
||||
expect(block).toMatch(/\$ rig workflow /); // at least one concrete example invocation
|
||||
});
|
||||
}
|
||||
|
||||
it("every verb has a non-empty description", () => {
|
||||
const root = workflowCommand();
|
||||
for (const sub of root.commands) {
|
||||
expect(
|
||||
sub.description().length,
|
||||
`verb ${sub.name()} needs a description`,
|
||||
).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("the top-level command itself is documented", () => {
|
||||
const root = workflowCommand();
|
||||
expect(root.description().length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("printOutcomeSummary — what/state/next ending (HG-4)", () => {
|
||||
function captureStdout(): { logs: string[]; restore: () => void } {
|
||||
const logs: string[] = [];
|
||||
const original = console.log;
|
||||
console.log = (...args: unknown[]) => {
|
||||
logs.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
||||
};
|
||||
return { logs, restore: () => { console.log = original; } };
|
||||
}
|
||||
|
||||
const baseSummary: OutcomeSummary = {
|
||||
what: "Closed QITEM-123 (handoff) and projected QITEM-456 to velocity-qa@openrig-velocity",
|
||||
state: "instance WF01ABC = active",
|
||||
next: "Inspect: rig queue show QITEM-456",
|
||||
};
|
||||
|
||||
it("emits 4 lines (blank + what + state + next) in human mode on success", () => {
|
||||
const out = captureStdout();
|
||||
printOutcomeSummary(false, 200, baseSummary);
|
||||
out.restore();
|
||||
const joined = out.logs.join("\n");
|
||||
expect(joined).toMatch(/what:\s+Closed QITEM-123/);
|
||||
expect(joined).toMatch(/state:\s+instance WF01ABC = active/);
|
||||
expect(joined).toMatch(/next:\s+Inspect: rig queue show QITEM-456/);
|
||||
// Blank separator line precedes the summary.
|
||||
expect(out.logs[0]).toBe("");
|
||||
});
|
||||
|
||||
it("suppresses output in --json mode (machine consumers stay clean)", () => {
|
||||
const out = captureStdout();
|
||||
printOutcomeSummary(true, 200, baseSummary);
|
||||
out.restore();
|
||||
expect(out.logs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("suppresses output when status >= 400 (error path uses its own surface)", () => {
|
||||
const out = captureStdout();
|
||||
printOutcomeSummary(false, 500, baseSummary);
|
||||
out.restore();
|
||||
expect(out.logs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("suppresses output when summary is null", () => {
|
||||
const out = captureStdout();
|
||||
printOutcomeSummary(false, 200, null);
|
||||
out.restore();
|
||||
expect(out.logs.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,11 @@ import { GETTING_STARTED_NARRATIVE } from "./getting-started-narrative.js";
|
||||
|
||||
type DefaultSlice = {
|
||||
id: string;
|
||||
/** §1 dot-ID per conventions/scope-and-versioning. Minted into the
|
||||
* slice README frontmatter so the seeded scaffold satisfies the
|
||||
* GA-convention coherence target without needing to round-trip
|
||||
* through `rig scope`. */
|
||||
dotId: string;
|
||||
title: string;
|
||||
status: "active" | "draft";
|
||||
objective: string;
|
||||
@@ -12,27 +17,37 @@ type DefaultSlice = {
|
||||
|
||||
type DefaultMission = {
|
||||
id: string;
|
||||
/** §1 mission dot-ID — escape band for non-release missions. */
|
||||
dotId: string;
|
||||
title: string;
|
||||
status: "active" | "draft";
|
||||
objective: string;
|
||||
slices: DefaultSlice[];
|
||||
};
|
||||
|
||||
// Release-0.3.2 slice 01 (OPR.0.3.2.1) — seeded missions/slices now
|
||||
// carry §1 dot-IDs in frontmatter so the scaffold coheres with
|
||||
// `conventions/scope-and-versioning`. `getting-started` is a
|
||||
// non-release mission → escape band `OPR.99.0.1`; its slices fan out
|
||||
// as `OPR.99.0.1.<n>` (numbers monotonic, never reused).
|
||||
const DEFAULT_MISSIONS: DefaultMission[] = [
|
||||
{
|
||||
id: "getting-started",
|
||||
dotId: "OPR.99.0.1",
|
||||
title: "Getting Started",
|
||||
status: "active",
|
||||
objective: "Launch the conveyor starter and learn how OpenRig moves work through queue-backed slices.",
|
||||
slices: [
|
||||
{
|
||||
id: "first-conveyor-run",
|
||||
dotId: "OPR.99.0.1.1",
|
||||
title: "First Conveyor Run",
|
||||
status: "active",
|
||||
objective: "Move one small packet through intake, planning, build, review, and close on the conveyor starter.",
|
||||
},
|
||||
{
|
||||
id: "inspect-project-evidence",
|
||||
dotId: "OPR.99.0.1.2",
|
||||
title: "Inspect Project Evidence",
|
||||
status: "draft",
|
||||
objective: "Open Project, Queue, Story, and Tests to inspect the evidence created by the first conveyor run.",
|
||||
@@ -61,7 +76,7 @@ export function workspaceScaffoldDirs(): string[] {
|
||||
function subdirReadmeContent(subdir: string): string {
|
||||
switch (subdir) {
|
||||
case "missions":
|
||||
return "# missions\n\nProject missions live here. Each mission folder maps to one Project mission in the UI and owns a `slices/` child folder.\n\nExpected shape:\n\n```text\nmissions/<mission-id>/README.md\nmissions/<mission-id>/PROGRESS.md\nmissions/<mission-id>/slices/<slice-id>/README.md\n```\n";
|
||||
return "# missions\n\nProject missions live here. Each mission folder maps to one Project mission in the UI and owns a `slices/` child folder.\n\nExpected shape:\n\n```text\nmissions/<mission-name>/README.md\nmissions/<mission-name>/PROGRESS.md\nmissions/<mission-name>/slices/<slice-name>/README.md\n```\n\nEvery mission and slice README carries a stable dot-ID in its frontmatter (`id: OPR.<ver>[.<n>]`) per the scope-and-versioning convention. Use `rig scope mission create <name>` and `rig scope slice create <mission> <slug>` to mint conformant artifacts; the CLI handles the dot-ID + auto-numbering for you. The folder name is the operator-facing slug; the `id:` is the stable handle that survives renames.\n";
|
||||
case "artifacts":
|
||||
return "# artifacts\n\nWork products live here: plans, drafts, generated outputs, and other files that a slice may reference before closure.\n";
|
||||
case "evidence":
|
||||
@@ -90,7 +105,7 @@ This workspace is file-backed. The Project UI mirrors this structure:
|
||||
- \`evidence/\` is for proof notes and verification summaries.
|
||||
- \`dogfood-evidence/<proof-packet-id>\` becomes Tests proof when the packet id contains the slice id tokens.
|
||||
|
||||
Use stable kebab-case ids for mission and slice folders. Keep slice ids unique inside the workspace.
|
||||
Use stable kebab-case names for mission and slice folders. Every mission/slice README also carries a stable dot-ID in its frontmatter (\`id: OPR.<ver>[.<n>]\`) per the \`scope-and-versioning\` convention; mint conformant artifacts via \`rig scope mission create <name>\` and \`rig scope slice create <mission> <slug>\`. Folder names are operator-facing slugs; the \`id:\` is the rename-proof handle.
|
||||
`;
|
||||
|
||||
const STEERING_PLACEHOLDER = `---
|
||||
@@ -121,6 +136,7 @@ alongside your Project workspace.
|
||||
|
||||
function missionReadme(mission: DefaultMission): string {
|
||||
return `---
|
||||
id: ${mission.dotId}
|
||||
title: ${mission.title}
|
||||
status: ${mission.status}
|
||||
mission: ${mission.id}
|
||||
@@ -157,6 +173,7 @@ function sliceReadme(mission: DefaultMission, slice: DefaultSlice): string {
|
||||
const narrative = GETTING_STARTED_NARRATIVE[slice.id];
|
||||
if (narrative) {
|
||||
return `---
|
||||
id: ${slice.dotId}
|
||||
title: ${slice.title}
|
||||
status: ${slice.status}
|
||||
mission: ${mission.id}
|
||||
@@ -167,6 +184,7 @@ slice: ${slice.id}
|
||||
${narrative.readme}`;
|
||||
}
|
||||
return `---
|
||||
id: ${slice.dotId}
|
||||
title: ${slice.title}
|
||||
status: ${slice.status}
|
||||
mission: ${mission.id}
|
||||
|
||||
Reference in new issue
Block a user