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:
mvschwarzandClaude Opus 4.7 committed 2026-05-16 02:39:48 -07:00
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}
+141 -3
View File
@@ -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}`,
});
});
});
+128
View File
@@ -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}