feat(cli): authored-summary teaching layer on the --summary rail [P3]

PM ruling (the convention without its teaching layer is just hope): the shipped
warn-then-require rail (OPR.0.4.1.18 FR-7) told callers a missing --summary COSTS a Story-node
truncation. Add the TEACHING LAYER — the stderr advisory + the --summary hint now also TEACH the
convention: a good summary is 1-2 plain sentences a human skims in the needs-you view (what the
work is and why it needs this seat), not the agent-speak --body. Applied across all three
authoring surfaces (create / handoff / handoff-and-complete), advisory + hint.

Rails held: warn->stderr (json stdout clean); warn-not-require (omission still creates the qitem,
pre-18 exempt) pinned; the two handoff advisories stay BYTE-IDENTICAL (one replace_all + a parity
pin); facts-not-fabricated (teaches, never invents; served-field fallback untouched); single-parse
(pure static strings, no downstream re-parse).

RED-first: 3 pins (create advisory teaches + no-hard-break; handoff/h-and-c advisories teach +
byte-identical parity; create --summary hint teaches). RED at base -> GREEN. queue suite 83/83,
tsc 0, build 0. Never push.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Opus 4.8 committed 2026-08-06 13:23:46 +00:00
1 parent 97a181328f
commit b2435e68a6
2 files changed
+79 -6

No files matched your search

+6 -6
View File
@@ -296,7 +296,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
.option("--expires-at <iso>", "ISO timestamp at which the qitem expires")
.option("--id <qitemId>", "Idempotent qitem_id (skip if not provided)")
.option("--target-repo <name>", "PL-007: typed repo scope (must match a repo in the source rig's RigSpec.workspace.repos[])")
.option("--summary <text>", "OPR.0.4.1.18: short human-readable 1-2 sentence summary of the work (feeds the Story node label; the agent-speak --body stays the source of truth). Warned-if-missing; pre-18 qitems exempt.")
.option("--summary <text>", "OPR.0.4.1.18: short human-readable 1-2 sentence summary of the work — what it is and why this seat, skimmable by a human in the needs-you view (the agent-speak --body stays the source of truth). Warned-if-missing; pre-18 qitems exempt.")
.option("--evidence-ref <path>", "OPR.0.4.4.19 FR-5: pointer to the durable artifact a human judges (e.g. a PROOF.md path). Required by the daemon when the item is human-routed; optional otherwise.")
.option("--host <id>", QUEUE_HOST_OPTION_HELP)
.option("--no-nudge", "Suppress the default destination nudge (cold-queue)")
@@ -355,7 +355,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
// callers that omit it; hard-require is a future hardening.
if (!opts.summary) {
process.stderr.write(
"warning: rig queue create called without --summary. New qitems should carry a short human-readable summary; the Story node degrades to a body truncation without it. Proceeding (pre-18 callers exempt).\n"
"warning: rig queue create called without --summary. New qitems should carry a short human-readable summary; the Story node degrades to a body truncation without it. A good summary is 1-2 plain sentences a human skims in the needs-you view — what the work is and why it needs this seat, not the agent-speak --body. Proceeding (pre-18 callers exempt).\n"
);
}
const deps = getDeps();
@@ -585,7 +585,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
.option("--tags <tags>", "Comma-separated tags for the new qitem")
.option("--gate <role>", "OPR.0.4.3.16: mark the new qitem as gate work; translated to a gate:<role> tag (e.g. guard | spec-review). The idle-gate watchdog reads this predicate. Composes with --tags.")
.option("--target-repo <name>", "PL-007: typed repo scope for the new qitem")
.option("--summary <text>", "OPR.0.4.1.18: short human-readable 1-2 sentence summary for the new qitem (feeds the Story node; --body stays source of truth). Warned-if-missing.")
.option("--summary <text>", "OPR.0.4.1.18: short human-readable 1-2 sentence summary for the new qitem — what it is and why this seat, skimmable in the needs-you view (--body stays source of truth). Warned-if-missing.")
.option("--evidence-ref <path>", "OPR.0.4.4.19 FR-5: durable-artifact pointer for the new qitem. Required by the daemon when the new qitem is human-routed; optional otherwise.")
.option("--host <id>", QUEUE_HOST_OPTION_HELP)
.option("--no-nudge", "Suppress the default nudge to the new destination")
@@ -633,7 +633,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
// it should carry its own summary. Warn to stderr; do not hard-break.
if (!opts.summary) {
process.stderr.write(
"warning: rig queue handoff called without --summary. The new qitem should carry a short human-readable summary; the Story node degrades to a body truncation without it. Proceeding.\n"
"warning: rig queue handoff called without --summary. The new qitem should carry a short human-readable summary; the Story node degrades to a body truncation without it. A good summary is 1-2 plain sentences a human skims in the needs-you view — what the work is and why it needs this seat, not the agent-speak --body. Proceeding.\n"
);
}
const deps = getDeps();
@@ -680,7 +680,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
.option("--tags <tags>", "Comma-separated tags for the new qitem")
.option("--gate <role>", "OPR.0.4.3.16: mark the new qitem as gate work; translated to a gate:<role> tag (e.g. guard | spec-review). The idle-gate watchdog reads this predicate. Composes with --tags.")
.option("--target-repo <name>", "PL-007: typed repo scope for the new qitem")
.option("--summary <text>", "OPR.0.4.1.18: short human-readable 1-2 sentence summary for the new qitem (feeds the Story node; --body stays source of truth). Warned-if-missing.")
.option("--summary <text>", "OPR.0.4.1.18: short human-readable 1-2 sentence summary for the new qitem — what it is and why this seat, skimmable in the needs-you view (--body stays source of truth). Warned-if-missing.")
.option("--evidence-ref <path>", "OPR.0.4.4.19 FR-5: durable-artifact pointer for the new qitem. Required by the daemon when the new qitem is human-routed; optional otherwise.")
.option("--host <id>", QUEUE_HOST_OPTION_HELP)
.option("--no-nudge", "Suppress the default nudge to the new destination")
@@ -726,7 +726,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
// it should carry its own summary. Warn to stderr; do not hard-break.
if (!opts.summary) {
process.stderr.write(
"warning: rig queue handoff called without --summary. The new qitem should carry a short human-readable summary; the Story node degrades to a body truncation without it. Proceeding.\n"
"warning: rig queue handoff called without --summary. The new qitem should carry a short human-readable summary; the Story node degrades to a body truncation without it. A good summary is 1-2 plain sentences a human skims in the needs-you view — what the work is and why it needs this seat, not the agent-speak --body. Proceeding.\n"
);
}
const deps = getDeps();
+73
View File
@@ -1383,3 +1383,76 @@ describe("rig queue CLI", () => {
});
});
});
// P3 — authored-summary TEACHING LAYER. The shipped warn-then-require rail (OPR.0.4.1.18
// FR-7) told callers a missing --summary COSTS a Story-node truncation; the teaching layer
// additionally TEACHES the convention (what a good summary is + where a human reads it), so
// the needs-you rows a human skims are actually authored, not just hoped for. Rails held:
// warn → stderr (json stdout clean), warn-not-hard-break, the two handoff advisories stay
// byte-identical, facts-not-fabricated (teaches, never invents).
describe("P3 — authored-summary teaching layer (advisory + --summary hint)", () => {
let stderrOut: string[];
const TEACHES_WHERE = /needs-you view/; // teaches WHERE the summary is read
const TEACHES_WHY = /why it needs this seat/; // teaches the WHAT+WHY convention
beforeEach(() => {
stderrOut = [];
vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => {
stderrOut.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString());
return true;
});
process.exitCode = undefined;
});
it("create without --summary TEACHES the convention on the stderr advisory (not just the truncation cost) and does NOT hard-break", async () => {
const { deps, calls } = makeDeps({
routes: { "POST /api/queue/create": { status: 201, data: { qitemId: "q1", state: "pending" } } },
});
const program = createProgram({ queueDeps: deps });
program.exitOverride();
await program.parseAsync([
"node", "rig", "queue", "create",
"--source", "a@rig", "--destination", "b@rig", "--body", "x", "--json",
]);
const warn = stderrOut.join("");
expect(warn).toMatch(TEACHES_WHERE);
expect(warn).toMatch(TEACHES_WHY);
// warn-not-require rail: the qitem is still created (no hard-break on omission).
expect(calls.find((c) => c.path === "/api/queue/create")).toBeDefined();
});
it("handoff + handoff-and-complete without --summary carry the SAME teaching advisory (byte-identical parity)", async () => {
async function warnFor(sub: "handoff" | "handoff-and-complete"): Promise<string> {
stderrOut = [];
const { deps } = makeDeps({
routes: {
"POST /api/queue/handoff": { status: 201, data: { qitemId: "q2" } },
"POST /api/queue/handoff-and-complete": { status: 201, data: { qitemId: "q3" } },
},
});
const program = createProgram({ queueDeps: deps });
program.exitOverride();
try {
await program.parseAsync([
"node", "rig", "queue", sub, "qitem-src-1",
"--from", "a@rig", "--to", "b@rig", "--body", "y", "--json",
]);
} catch { /* exitOverride / downstream mock — the warn fires before the daemon call */ }
return stderrOut.join("");
}
const h = await warnFor("handoff");
const hc = await warnFor("handoff-and-complete");
expect(h).toMatch(TEACHES_WHERE);
expect(h).toMatch(TEACHES_WHY);
expect(hc).toBe(h); // parity rail: the two handoff advisories are byte-identical
});
it("the create --summary option hint teaches WHERE the summary is read (not only the cost)", () => {
const { deps } = makeDeps();
const program = createProgram({ queueDeps: deps });
const queueCmd = program.commands.find((c) => c.name() === "queue")!;
const createCmd = queueCmd.commands.find((c) => c.name() === "create")!;
const summaryOpt = createCmd.options.find((o) => o.long === "--summary")!;
expect(summaryOpt.description).toMatch(TEACHES_WHERE);
});
});