Skip to content

Commit 4325bcd

Browse files
Merge pull request #1136 from corbitsdev/cl-8269-extract-prompt-variance-as-a-versioned-package-agent-owned
feat(prompt-variance): extract family prompt variance as a versioned package
2 parents a98cf9d + 035835a commit 4325bcd

14 files changed

Lines changed: 606 additions & 64 deletions

‎bun.lock‎

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "@corbits/prompt-variance",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"license": "SEE LICENSE IN LICENSE.md",
7+
"exports": {
8+
".": {
9+
"types": "./src/index.ts",
10+
"default": "./src/index.ts"
11+
}
12+
}
13+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { assemble } from "./assemble.js";
3+
import { claudeRow, defaultRow, gptRow, grokRow, museRow } from "./rows.js";
4+
5+
const SECTIONS = ["contract", "Tools (names only): read_file", "context"];
6+
7+
describe("assemble", () => {
8+
test("joins sections with the residual as the last section", () => {
9+
const out = assemble(SECTIONS, grokRow);
10+
expect(out).toContain("contract");
11+
expect(out.trimEnd().endsWith(grokRow.residual)).toBe(true);
12+
});
13+
14+
test("a row without residual leaves the sections unchanged", () => {
15+
expect(assemble(SECTIONS, defaultRow)).toBe(SECTIONS.join("\n\n"));
16+
});
17+
18+
test("muse residual closes the prompt like the shipped tail append", () => {
19+
const out = assemble(SECTIONS, museRow);
20+
expect(out.trimEnd().endsWith(museRow.residual)).toBe(true);
21+
});
22+
23+
test("claude and gpt residuals close the prompt the same way", () => {
24+
expect(
25+
assemble(SECTIONS, claudeRow).trimEnd().endsWith(claudeRow.residual),
26+
).toBe(true);
27+
expect(assemble(SECTIONS, gptRow).trimEnd().endsWith(gptRow.residual)).toBe(
28+
true,
29+
);
30+
});
31+
32+
test("tool mounting is out of scope: assemble returns a string, not tool names", () => {
33+
const out = assemble(SECTIONS, grokRow);
34+
expect(typeof out).toBe("string");
35+
});
36+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { PromptVarianceRow } from "./rows.js";
2+
3+
/**
4+
* Append a family variance row's residual at the tail of the assembled
5+
* sections (CL-8269). An empty residual leaves the sections unchanged.
6+
* Residuals render last so they cannot disturb the cached prompt prefix.
7+
* Tool mounting is out of scope: run.ts applies
8+
* ModelFamilyPolicy.advertisedToolDeny at mount time.
9+
*/
10+
export function assemble(
11+
sections: readonly string[],
12+
familyRow: PromptVarianceRow,
13+
): string {
14+
if (familyRow.residual.length === 0) return sections.join("\n\n");
15+
return [...sections, familyRow.residual].join("\n\n");
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export { assemble } from "./assemble.js";
2+
export { resolvePromptVariance } from "./resolve.js";
3+
export {
4+
claudeRow,
5+
defaultRow,
6+
FAMILY_IDS,
7+
FAMILY_ROWS,
8+
gptRow,
9+
grokRow,
10+
grokToolBudgetResidual,
11+
museRow,
12+
type PromptVarianceFamily,
13+
type PromptVarianceRow,
14+
} from "./rows.js";
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { resolvePromptVariance } from "./resolve.js";
3+
import { grokRow } from "./rows.js";
4+
5+
describe("resolvePromptVariance", () => {
6+
test("resolves each family to its row", () => {
7+
expect(resolvePromptVariance({ family: "default" }).id).toBe("default");
8+
expect(resolvePromptVariance({ family: "muse" }).id).toBe("muse");
9+
expect(resolvePromptVariance({ family: "grok" }).id).toBe("grok");
10+
expect(resolvePromptVariance({ family: "claude" }).id).toBe("claude");
11+
expect(resolvePromptVariance({ family: "gpt" }).id).toBe("gpt");
12+
});
13+
14+
test("grok keeps its finish-bias residual on leaves", () => {
15+
const row = resolvePromptVariance({ family: "grok", orchestrator: false });
16+
expect(row.residual).toBe(grokRow.residual);
17+
});
18+
19+
test("grok on orchestrators falls back to the default row", () => {
20+
const row = resolvePromptVariance({ family: "grok", orchestrator: true });
21+
expect(row.id).toBe("default");
22+
expect(row.residual).toBe("");
23+
});
24+
25+
test("claude keeps its task_guidance block on leaves, not orchestrators", () => {
26+
expect(
27+
resolvePromptVariance({ family: "claude", orchestrator: false }).residual,
28+
).toContain("<task_guidance>");
29+
const orch = resolvePromptVariance({
30+
family: "claude",
31+
orchestrator: true,
32+
});
33+
expect(orch.id).toBe("default");
34+
expect(orch.residual).toBe("");
35+
});
36+
37+
test("muse keeps its residual on leaves and orchestrators alike", () => {
38+
expect(
39+
resolvePromptVariance({ family: "muse", orchestrator: false }).residual,
40+
).not.toBe("");
41+
expect(
42+
resolvePromptVariance({ family: "muse", orchestrator: true }).residual,
43+
).not.toBe("");
44+
});
45+
46+
test("gpt keeps its narrate-before-tools nudge on leaves and orchestrators alike", () => {
47+
expect(
48+
resolvePromptVariance({ family: "gpt", orchestrator: false }).residual,
49+
).toContain("Narrate before tools");
50+
expect(
51+
resolvePromptVariance({ family: "gpt", orchestrator: true }).residual,
52+
).toContain("Narrate before tools");
53+
});
54+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {
2+
defaultRow,
3+
FAMILY_ROWS,
4+
type PromptVarianceFamily,
5+
type PromptVarianceRow,
6+
} from "./rows.js";
7+
8+
/**
9+
* Resolve the variance row for a family, mirroring the leaves-only gates in
10+
* the model family policy: the grok finish-bias residual and the claude
11+
* task_guidance block only make sense on leaf workers, so orchestrators
12+
* fall back to the default row. Muse keeps its residual on both — the
13+
* constructors append the same text at the tail either way — and gpt keeps
14+
* its narrate-before-tools nudge on primaries and leaves alike.
15+
*/
16+
export function resolvePromptVariance(input: {
17+
family: PromptVarianceFamily;
18+
orchestrator?: boolean;
19+
}): PromptVarianceRow {
20+
if (input.orchestrator === true) {
21+
if (input.family === "grok" || input.family === "claude") {
22+
return defaultRow;
23+
}
24+
}
25+
return FAMILY_ROWS[input.family];
26+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { describe, expect, test } from "bun:test";
2+
import {
3+
claudeRow,
4+
defaultRow,
5+
FAMILY_IDS,
6+
FAMILY_ROWS,
7+
gptRow,
8+
grokRow,
9+
grokToolBudgetResidual,
10+
museRow,
11+
type PromptVarianceFamily,
12+
} from "./rows.js";
13+
14+
const CEREMONY_LINES = [
15+
"Never run git add, git commit, git stash, or any other state-changing git command unless the user asks.",
16+
"Do not narrate a plan before acting on a small task; act, then report.",
17+
"Verify with the test command once at the end, not after every edit.",
18+
];
19+
20+
describe("prompt-variance family rows", () => {
21+
test("ships exactly the default/muse/grok/claude/gpt families", () => {
22+
expect([...FAMILY_IDS]).toEqual([
23+
"default",
24+
"muse",
25+
"grok",
26+
"claude",
27+
"gpt",
28+
]);
29+
});
30+
31+
test("has no glm row until its eval lands (CL-8265)", () => {
32+
for (const id of FAMILY_IDS) {
33+
expect(id).not.toBe("glm");
34+
}
35+
});
36+
37+
test("every row carries the id/residual shape — residuals-only, no deny fields", () => {
38+
for (const row of [defaultRow, museRow, grokRow, claudeRow, gptRow]) {
39+
expect(typeof row.id).toBe("string");
40+
expect(typeof row.residual).toBe("string");
41+
expect("advertisedToolDeny" in row).toBe(false);
42+
expect("sectionOmit" in row).toBe(false);
43+
expect("overrides" in row).toBe(false);
44+
}
45+
});
46+
47+
test("row ids match their family and FAMILY_ROWS covers all five", () => {
48+
const ids: PromptVarianceFamily[] = [
49+
defaultRow.id,
50+
museRow.id,
51+
grokRow.id,
52+
claudeRow.id,
53+
gptRow.id,
54+
];
55+
expect(ids).toEqual(["default", "muse", "grok", "claude", "gpt"]);
56+
expect(Object.keys(FAMILY_ROWS).sort()).toEqual([
57+
"claude",
58+
"default",
59+
"gpt",
60+
"grok",
61+
"muse",
62+
]);
63+
});
64+
65+
test("muse row is the shipped CL-7869 tool-discipline text", () => {
66+
expect(museRow.residual).toContain("Tool discipline:");
67+
expect(museRow.residual).toContain("Batch independent tool calls");
68+
expect(museRow.residual).toContain("Never re-read a file");
69+
expect(museRow.residual).toContain("Do not narrate; act.");
70+
});
71+
72+
test("grok row is the single finish-bias + ceremony block (CL-8296)", () => {
73+
expect(grokRow.residual).toContain("Finish bias (xAI / Grok worker):");
74+
expect(grokRow.residual).toContain("prefer the structured report");
75+
expect(grokRow.residual).toContain("re-open paths you already read");
76+
expect(grokRow.residual).toContain("done-definition is met");
77+
expect(grokRow.residual).toContain("never run_shell");
78+
});
79+
80+
test("each ceremony line appears exactly once in the grok row (P2 invariant)", () => {
81+
for (const line of CEREMONY_LINES) {
82+
const occurrences = grokRow.residual.split(line).length - 1;
83+
expect(occurrences).toBe(1);
84+
}
85+
});
86+
87+
test("grok tool-budget residual is ceremony-free and budget text appears once (P1 invariant)", () => {
88+
expect(grokToolBudgetResidual).toContain("Tool budget:");
89+
for (const line of CEREMONY_LINES) {
90+
expect(grokToolBudgetResidual).not.toContain(line);
91+
}
92+
const budgetOccurrences =
93+
grokToolBudgetResidual.split("Tool budget:").length - 1;
94+
expect(budgetOccurrences).toBe(1);
95+
});
96+
97+
test("default row carries no residual", () => {
98+
expect(defaultRow.residual).toBe("");
99+
});
100+
101+
test("claude row is the single XML task_guidance block (CL-8309)", () => {
102+
expect(claudeRow.residual).toContain("<task_guidance>");
103+
expect(claudeRow.residual).toContain("</task_guidance>");
104+
expect(claudeRow.residual).toContain("Follow the dispatch brief exactly");
105+
expect(claudeRow.residual).toContain("Batch independent tool calls");
106+
});
107+
108+
test("gpt row is the narrate-before-tools nudge (CL-8310)", () => {
109+
expect(gptRow.residual).toContain("Narrate before tools (GPT worker):");
110+
expect(gptRow.residual).toContain(
111+
"one short line saying what you are doing",
112+
);
113+
expect(gptRow.residual).toContain("done-definition is met");
114+
});
115+
});

0 commit comments

Comments
 (0)