Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions scripts/construct.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3968,7 +3968,8 @@ function analyzeRun(runDir) {
const brief = loadBrief(runDir);
const evidence = loadEvidence3(runDir);
const notes = loadMetaNotes(runDir);
const drill = (cmd, q) => `construct ${cmd} --out ${runDir} --q "${q.replace(/"/g, "'")}"`;
const shellQuote = (s) => `'${s.replace(/\r\n|[\r\n]/g, " ").replace(/'/g, `'"'"'`)}'`;
const drill = (cmd, q) => `construct ${cmd} --out ${runDir} --q ${shellQuote(q)}`;
const bySource = {};
for (const e of evidence) bySource[e.source] = (bySource[e.source] ?? 0) + 1;
if (evidence.length === 0) {
Expand Down Expand Up @@ -4426,7 +4427,8 @@ function agentContracts(runAbs, engineAbs, idea) {
runAbs,
"Your ONE sanctioned write surface is your own isolated git worktree \u2014 app code and app tests only. The run folder (BUILD-PLAN.json, SRD.json, evidence/) stays the orchestrator's."
);
const product = idea ? `\`${idea}\`` : "(no brief.json yet \u2014 the orchestrator will restate the one-liner in your prompt)";
const inlineIdea = idea.replace(/`/g, "'").replace(/\s*(?:\r\n|[\r\n])\s*/g, " ").trim();
const product = inlineIdea ? `\`${inlineIdea}\`` : "(no brief.json yet \u2014 the orchestrator will restate the one-liner in your prompt)";
return {
researcher: `# Contract: researcher

Expand All @@ -4446,7 +4448,7 @@ ${footer}`,

You are an adversarial skeptic verifying that each SRD claim is actually SUPPORTED by the evidence it cites (references/orchestration.md Pattern 4). Assume the citation is decorative until the evidence proves otherwise.

Worklist: \`${join15(runAbs, "VERIFY.todo.json")}\` (\`{ pairs: [...] }\`; each pair has \`claimId\`, \`kind\`, \`claim\`, \`evidenceId\`, \`source\`, \`digest\`). Handle ONLY the pairs whose \`claimId::evidenceId\` key is named in your prompt (\`PAIRS=<key,\u2026>\`).
Worklist: \`${join15(runAbs, "VERIFY.todo.json")}\` (\`{ pairs: [...] }\`; each pair has \`claimId\`, \`kind\`, \`claim\`, \`evidenceId\`, \`source\`, \`digest\`). Handle ONLY the pairs whose \`claimId::evidenceId\` key is named in your prompt (\`PAIRS=<key,\u2026>\`). If a PAIRS key is no longer in the worklist, skip it and say so in your note.

For EACH of your pairs:

Expand Down Expand Up @@ -4477,7 +4479,7 @@ Return (structured output): \`{ "lens", "score", "rationale" }\` \u2014 a 1\u201
${footer}`,
builder: `# Contract: builder

You build ONE task of \`${join15(runAbs, "BUILD-PLAN.json")}\`, test-first, in your OWN isolated git worktree (references/orchestration.md Pattern 5 + references/build-playbook.md). Your prompt names your task (\`TASK=<id>\`).
You build ONE task of \`${join15(runAbs, "BUILD-PLAN.json")}\`, test-first, in your OWN isolated git worktree (references/orchestration.md Pattern 5 + references/build-playbook.md). Your prompt names your task (\`TASK=<id>\`). If your TASK id is no longer in the worklist, skip it and say so in your summary.

1. Read your task in the plan. Its \`acceptance\` entries POINT into \`${join15(runAbs, "SRD.json")}\` (\`functional[frId].acceptance[index]\`) \u2014 the SRD stays the single source of truth for what "done" means.
2. Work ONLY inside your own git worktree (the workflow dispatches you with \`isolation: 'worktree'\`). TDD each acceptance criterion: failing test first, then make it pass \u2014 and **every test names its FR id** (e.g. \`describe("FR-001 \u2026")\`; that is what \`verify\` greps for).
Expand Down
10 changes: 6 additions & 4 deletions skills/construct/scripts/construct.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3968,7 +3968,8 @@ function analyzeRun(runDir) {
const brief = loadBrief(runDir);
const evidence = loadEvidence3(runDir);
const notes = loadMetaNotes(runDir);
const drill = (cmd, q) => `construct ${cmd} --out ${runDir} --q "${q.replace(/"/g, "'")}"`;
const shellQuote = (s) => `'${s.replace(/\r\n|[\r\n]/g, " ").replace(/'/g, `'"'"'`)}'`;
const drill = (cmd, q) => `construct ${cmd} --out ${runDir} --q ${shellQuote(q)}`;
const bySource = {};
for (const e of evidence) bySource[e.source] = (bySource[e.source] ?? 0) + 1;
if (evidence.length === 0) {
Expand Down Expand Up @@ -4426,7 +4427,8 @@ function agentContracts(runAbs, engineAbs, idea) {
runAbs,
"Your ONE sanctioned write surface is your own isolated git worktree \u2014 app code and app tests only. The run folder (BUILD-PLAN.json, SRD.json, evidence/) stays the orchestrator's."
);
const product = idea ? `\`${idea}\`` : "(no brief.json yet \u2014 the orchestrator will restate the one-liner in your prompt)";
const inlineIdea = idea.replace(/`/g, "'").replace(/\s*(?:\r\n|[\r\n])\s*/g, " ").trim();
const product = inlineIdea ? `\`${inlineIdea}\`` : "(no brief.json yet \u2014 the orchestrator will restate the one-liner in your prompt)";
return {
researcher: `# Contract: researcher

Expand All @@ -4446,7 +4448,7 @@ ${footer}`,

You are an adversarial skeptic verifying that each SRD claim is actually SUPPORTED by the evidence it cites (references/orchestration.md Pattern 4). Assume the citation is decorative until the evidence proves otherwise.

Worklist: \`${join15(runAbs, "VERIFY.todo.json")}\` (\`{ pairs: [...] }\`; each pair has \`claimId\`, \`kind\`, \`claim\`, \`evidenceId\`, \`source\`, \`digest\`). Handle ONLY the pairs whose \`claimId::evidenceId\` key is named in your prompt (\`PAIRS=<key,\u2026>\`).
Worklist: \`${join15(runAbs, "VERIFY.todo.json")}\` (\`{ pairs: [...] }\`; each pair has \`claimId\`, \`kind\`, \`claim\`, \`evidenceId\`, \`source\`, \`digest\`). Handle ONLY the pairs whose \`claimId::evidenceId\` key is named in your prompt (\`PAIRS=<key,\u2026>\`). If a PAIRS key is no longer in the worklist, skip it and say so in your note.

For EACH of your pairs:

Expand Down Expand Up @@ -4477,7 +4479,7 @@ Return (structured output): \`{ "lens", "score", "rationale" }\` \u2014 a 1\u201
${footer}`,
builder: `# Contract: builder

You build ONE task of \`${join15(runAbs, "BUILD-PLAN.json")}\`, test-first, in your OWN isolated git worktree (references/orchestration.md Pattern 5 + references/build-playbook.md). Your prompt names your task (\`TASK=<id>\`).
You build ONE task of \`${join15(runAbs, "BUILD-PLAN.json")}\`, test-first, in your OWN isolated git worktree (references/orchestration.md Pattern 5 + references/build-playbook.md). Your prompt names your task (\`TASK=<id>\`). If your TASK id is no longer in the worklist, skip it and say so in your summary.

1. Read your task in the plan. Its \`acceptance\` entries POINT into \`${join15(runAbs, "SRD.json")}\` (\`functional[frId].acceptance[index]\`) \u2014 the SRD stays the single source of truth for what "done" means.
2. Work ONLY inside your own git worktree (the workflow dispatches you with \`isolation: 'worktree'\`). TDD each acceptance criterion: failing test first, then make it pass \u2014 and **every test names its FR id** (e.g. \`describe("FR-001 \u2026")\`; that is what \`verify\` greps for).
Expand Down
7 changes: 6 additions & 1 deletion src/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ export function analyzeRun(runDir: string): GapReport {
const brief = loadBrief(runDir);
const evidence = loadEvidence(runDir);
const notes = loadMetaNotes(runDir);
const drill = (cmd: string, q: string) => `construct ${cmd} --out ${runDir} --q "${q.replace(/"/g, "'")}"`;
// Drill commands embed brief free-text into a copy-paste / subagent-executed
// shell line: single-quote the query (the only shell-inert quoting — backticks
// and $() never expand inside single quotes), escape an embedded ' as '"'"'
// and flatten newlines, so a hostile brief field cannot inject.
const shellQuote = (s: string) => `'${s.replace(/\r\n|[\r\n]/g, " ").replace(/'/g, `'"'"'`)}'`;
const drill = (cmd: string, q: string) => `construct ${cmd} --out ${runDir} --q ${shellQuote(q)}`;

const bySource: Record<string, number> = {};
for (const e of evidence) bySource[e.source] = (bySource[e.source] ?? 0) + 1;
Expand Down
13 changes: 10 additions & 3 deletions src/orchestrate-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,14 @@ export function agentContracts(runAbs: string, engineAbs: string, idea: string):
runAbs,
"Your ONE sanctioned write surface is your own isolated git worktree — app code and app tests only. The run folder (BUILD-PLAN.json, SRD.json, evidence/) stays the orchestrator's.",
);
const product = idea ? `\`${idea}\`` : "(no brief.json yet — the orchestrator will restate the one-liner in your prompt)";
// The idea is interpolated into a Markdown inline-code span: an interior
// backtick would close the span early (and read as executable in copy-paste
// contexts) — strip backticks to apostrophes and flatten newlines.
const inlineIdea = idea
.replace(/`/g, "'")
.replace(/\s*(?:\r\n|[\r\n])\s*/g, " ")
.trim();
const product = inlineIdea ? `\`${inlineIdea}\`` : "(no brief.json yet — the orchestrator will restate the one-liner in your prompt)";
return {
researcher: `# Contract: researcher

Expand All @@ -262,7 +269,7 @@ ${footer}`,

You are an adversarial skeptic verifying that each SRD claim is actually SUPPORTED by the evidence it cites (references/orchestration.md Pattern 4). Assume the citation is decorative until the evidence proves otherwise.

Worklist: \`${join(runAbs, "VERIFY.todo.json")}\` (\`{ pairs: [...] }\`; each pair has \`claimId\`, \`kind\`, \`claim\`, \`evidenceId\`, \`source\`, \`digest\`). Handle ONLY the pairs whose \`claimId::evidenceId\` key is named in your prompt (\`PAIRS=<key,…>\`).
Worklist: \`${join(runAbs, "VERIFY.todo.json")}\` (\`{ pairs: [...] }\`; each pair has \`claimId\`, \`kind\`, \`claim\`, \`evidenceId\`, \`source\`, \`digest\`). Handle ONLY the pairs whose \`claimId::evidenceId\` key is named in your prompt (\`PAIRS=<key,…>\`). If a PAIRS key is no longer in the worklist, skip it and say so in your note.

For EACH of your pairs:

Expand Down Expand Up @@ -293,7 +300,7 @@ Return (structured output): \`{ "lens", "score", "rationale" }\` — a 1–5 int
${footer}`,
builder: `# Contract: builder

You build ONE task of \`${join(runAbs, "BUILD-PLAN.json")}\`, test-first, in your OWN isolated git worktree (references/orchestration.md Pattern 5 + references/build-playbook.md). Your prompt names your task (\`TASK=<id>\`).
You build ONE task of \`${join(runAbs, "BUILD-PLAN.json")}\`, test-first, in your OWN isolated git worktree (references/orchestration.md Pattern 5 + references/build-playbook.md). Your prompt names your task (\`TASK=<id>\`). If your TASK id is no longer in the worklist, skip it and say so in your summary.

1. Read your task in the plan. Its \`acceptance\` entries POINT into \`${join(runAbs, "SRD.json")}\` (\`functional[frId].acceptance[index]\`) — the SRD stays the single source of truth for what "done" means.
2. Work ONLY inside your own git worktree (the workflow dispatches you with \`isolation: 'worktree'\`). TDD each acceptance criterion: failing test first, then make it pass — and **every test names its FR id** (e.g. \`describe("FR-001 …")\`; that is what \`verify\` greps for).
Expand Down
4 changes: 2 additions & 2 deletions tests/__snapshots__/orchestrate.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ exports[`orchestrate — contracts & runbook > golden shape (paths normalized) >

You are an adversarial skeptic verifying that each SRD claim is actually SUPPORTED by the evidence it cites (references/orchestration.md Pattern 4). Assume the citation is decorative until the evidence proves otherwise.

Worklist: \`<RUN>/VERIFY.todo.json\` (\`{ pairs: [...] }\`; each pair has \`claimId\`, \`kind\`, \`claim\`, \`evidenceId\`, \`source\`, \`digest\`). Handle ONLY the pairs whose \`claimId::evidenceId\` key is named in your prompt (\`PAIRS=<key,…>\`).
Worklist: \`<RUN>/VERIFY.todo.json\` (\`{ pairs: [...] }\`; each pair has \`claimId\`, \`kind\`, \`claim\`, \`evidenceId\`, \`source\`, \`digest\`). Handle ONLY the pairs whose \`claimId::evidenceId\` key is named in your prompt (\`PAIRS=<key,…>\`). If a PAIRS key is no longer in the worklist, skip it and say so in your note.

For EACH of your pairs:

Expand Down Expand Up @@ -114,7 +114,7 @@ const RUN = "<RUN>"
const ENGINE = "<ENGINE>"
const WORKLIST = "<RUN>/evidence/evidence.json"
const AGENTS = RUN + '/orchestration/agents'
const BATCHES = [["feature (should): \\"Tag and organize saved articles\\" has no matchable evidence → drill: node <ENGINE> web --out <RUN> --q \\"Tag and organize saved articles\\"","feature (should): \\"Import an existing Pocket or Instapaper export\\" has no matchable evidence → drill: node <ENGINE> web --out <RUN> --q \\"Import an existing Pocket or Instapaper export\\"","feature (could): \\"Read articles offline on mobile\\" has no matchable evidence → drill: node <ENGINE> web --out <RUN> --q \\"Read articles offline on mobile\\"","tech: \\"PostgreSQL\\" has no docs/StackOverflow grounding → drill: node <ENGINE> tech --out <RUN> --q \\"PostgreSQL\\"","tech: \\"Next.js\\" has no docs/StackOverflow grounding → drill: node <ENGINE> tech --out <RUN> --q \\"Next.js\\"","oss seed: https://github.com/wallabag/wallabag yielded no mined evidence → drill: node <ENGINE> oss --out <RUN> --seeds https://github.com/wallabag/wallabag"]]
const BATCHES = [["feature (should): \\"Tag and organize saved articles\\" has no matchable evidence → drill: node <ENGINE> web --out <RUN> --q 'Tag and organize saved articles'","feature (should): \\"Import an existing Pocket or Instapaper export\\" has no matchable evidence → drill: node <ENGINE> web --out <RUN> --q 'Import an existing Pocket or Instapaper export'","feature (could): \\"Read articles offline on mobile\\" has no matchable evidence → drill: node <ENGINE> web --out <RUN> --q 'Read articles offline on mobile'","tech: \\"PostgreSQL\\" has no docs/StackOverflow grounding → drill: node <ENGINE> tech --out <RUN> --q 'PostgreSQL'","tech: \\"Next.js\\" has no docs/StackOverflow grounding → drill: node <ENGINE> tech --out <RUN> --q 'Next.js'","oss seed: https://github.com/wallabag/wallabag yielded no mined evidence → drill: node <ENGINE> oss --out <RUN> --seeds https://github.com/wallabag/wallabag"]]
const SCHEMA = {"type":"object","required":["findings"],"properties":{"findings":{"type":"array","items":{"type":"object","required":["gap","summary","urls"],"properties":{"gap":{"type":"string","description":"the gap label, verbatim from your prompt"},"summary":{"type":"string","description":"<=5 lines: what was found and why it matters to this product"},"urls":{"type":"array","items":{"type":"string"},"description":"URLs worth grounding, best first"}}}}}}

function contract(name, extra) {
Expand Down
13 changes: 12 additions & 1 deletion tests/analyze.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,18 @@ describe("analyzeRun — the post-research gap signal", () => {
it("reports a gap for a competitor the research never surfaced", () => {
const r = analyzeRun(makeRun({ briefOverride: { competitors: [...brief.competitors, "Zzyzx"] } }));
expect(r.unmatchedCompetitors).toEqual(["Zzyzx"]);
expect(r.suggestions.join("\n")).toMatch(/construct web --out .* --q "Zzyzx"/);
expect(r.suggestions.join("\n")).toMatch(/construct web --out .* --q 'Zzyzx'/);
});

it("single-quotes drill queries so shell metacharacters in brief free-text stay inert", () => {
const hostile = "pwn `whoami` $(id) and it's\nmultiline";
const out = makeRun({ briefOverride: { competitors: [...brief.competitors, hostile] } });
const r = analyzeRun(out);
const s = r.suggestions.find((x) => x.includes("pwn"));
expect(s).toBeDefined();
// Single-quoted for the shell: backticks and $() cannot expand, the embedded
// apostrophe is escaped as '"'"' and the newline is flattened to a space.
expect(s).toBe(`construct web --out ${out} --q 'pwn \`whoami\` $(id) and it'"'"'s multiline'`);
});

it("surfaces dossier notes (angle failures) from meta.json", () => {
Expand Down
21 changes: 21 additions & 0 deletions tests/orchestrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,27 @@ describe("orchestrate — contracts & runbook", () => {
expect(builder).toContain("BUILD-PLAN.json");
});

it("sanitizes backticks out of the idea so the contract's inline-code span survives", () => {
const run = makeRun({ review: true });
const raw = JSON.parse(readFileSync(join(run, "brief.json"), "utf8"));
raw.idea = "a `save-for-later` app\nwith `backticks`";
writeFileSync(join(run, "brief.json"), JSON.stringify(raw));
orchestrateRun(run, ENGINE);
const researcher = readFileSync(join(run, "orchestration", "agents", "researcher.md"), "utf8");
const line = researcher.split("\n").find((l) => l.startsWith("Product one-liner:"));
// The interpolated idea stays ONE inline-code span: no interior backtick may
// close it early, and the newline flattens to a space.
expect(line).toBe("Product one-liner: `a 'save-for-later' app with 'backticks'`");
});

it("worklist-driven contracts carry the family stale-id rule", () => {
const run = makeRun({ review: true, frontier: true });
orchestrateRun(run, ENGINE);
const read = (role: string) => readFileSync(join(run, "orchestration", "agents", `${role}.md`), "utf8");
expect(read("claim-reviewer")).toContain("If a PAIRS key is no longer in the worklist, skip it and say so in your note");
expect(read("builder")).toContain("If your TASK id is no longer in the worklist, skip it and say so in your summary");
});

it("the runbook covers every phase with concrete paths and the phase status", () => {
const run = makeRun({ review: true, frontier: true });
orchestrateRun(run, ENGINE);
Expand Down