mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 13:56:40 -10:00
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:
1 parent
97a181328f
commit
b2435e68a6
2 files changed
+79
-6
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user