Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1fb829c
feat(observations): observation criteria + pp-taxonomy handler, CLI, …
cedricvidal Jun 29, 2026
19b0299
feat(portal): observation criteria kind/taxonomy UI, submit picker, o…
cedricvidal Jun 29, 2026
78899c2
test(judge): live ATIF-trajectory observation acceptance (issue #1156)
cedricvidal Jun 29, 2026
c0a2093
fix(post-processor): copy handler.taxonomy.yaml into image so pp-taxo…
cedricvidal Jun 29, 2026
d5f866b
fix(compose): give post-processor JUDGE_SERVICE_URL so pp-taxonomy re…
cedricvidal Jun 29, 2026
7fde27f
docs(env): clarify post-processor JUDGE_SERVICE_URL wiring in compose…
cedricvidal Jun 29, 2026
7a639ad
fix(pp-taxonomy): read selected observations from request top-level, …
cedricvidal Jun 29, 2026
f689b01
feat(portal): rename run-detail Observations tab to Taxonomy with Obs…
cedricvidal Jun 29, 2026
85eb446
fix(portal): aggregate Enrichment badge over handler DAG + rename obs…
cedricvidal Jun 29, 2026
84f6e7b
fix(judge): unify trajectory navigation into one source-agnostic tool…
cedricvidal Jun 30, 2026
4c1785f
feat(shared): add criterion subject (run|iteration) + run-level obser…
cedricvidal Jun 30, 2026
01ec0a7
feat(api): validate criterion subject + AI auto-detect (#1156)
cedricvidal Jun 30, 2026
aa92dbe
feat(judge): merge whole-run trajectory from atifUrls[] (#1156)
cedricvidal Jun 30, 2026
4fc56b5
feat(pp-taxonomy): honor criterion subject — whole-run vs per-iterati…
cedricvidal Jun 30, 2026
1daf022
feat(portal): subject toggle + whole-run observations + steering copy…
cedricvidal Jun 30, 2026
f1b24e9
feat(cli): subject parity — --subject flag + whole-run observations (…
cedricvidal Jun 30, 2026
a7cfe9b
docs: criterion subject (run|iteration) + run-level observations (#1156)
cedricvidal Jun 30, 2026
a9e9dbd
merge: restack #1206 onto #1147
cedricvidal Oct 12, 2026
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
8 changes: 7 additions & 1 deletion ENV_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,13 @@ TTL for the DAG dispatcher's in-memory cache of handler topology (the `services`
**Default:** `post-processor-queue`
**Type:** string

Azure Storage Queue name used by both the scheduler (to enqueue post-processing work) and the post-processor worker (to dequeue). Must match between the two services.
Azure Storage Queue name used by both the scheduler (to enqueue post-processing work) and the post-processor worker (to dequeue). Must match between the two services. The `pp-taxonomy` observation handler shares this queue.

### JUDGE_URL
**Default:** falls back to `JUDGE_SERVICE_URL`, then `http://localhost:3002`
**Type:** URL string

Base URL of the judge service used by the `pp-taxonomy` post-process handler to evaluate observation criteria per iteration (snapshot + ATIF trajectory → boolean + evidence). The handler reuses the shared `JudgeClient`, so `JUDGE_CLIENT_TIMEOUT` / `JUDGE_CLIENT_RETRIES` apply. In K8s the post-processor reads `JUDGE_SERVICE_URL` from the `worker-config` ConfigMap (`envFrom`); in docker-compose the `post-processor` service sets `JUDGE_SERVICE_URL: http://judge:80` explicitly (it uses the `common-env` anchor, which — unlike `worker-env` — does not carry it). See issue #1156 and [docs/architecture/post-processing.md](docs/architecture/post-processing.md).

### SCHEDULER_URL
**Default:** (not set)
Expand Down
127 changes: 127 additions & 0 deletions apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,12 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = `
"judgeFeedback": {
"type": "string",
},
"observationResults": {
"items": {
"$ref": "#/components/schemas/CriterionResult",
},
"type": "array",
},
"passed": {
"type": "boolean",
},
Expand Down Expand Up @@ -682,9 +688,32 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = `
"pattern": "^[a-z][a-z0-9_]*$",
"type": "string",
},
"kind": {
"default": "gate",
"enum": [
"gate",
"observation",
],
"type": "string",
},
"prompt": {
"type": "string",
},
"subject": {
"enum": [
"run",
"iteration",
],
"type": "string",
},
"taxonomyElementId": {
"enum": [
"dimension:idiomatic-use",
"dimension:dependency-currency",
"dimension:configuration-correctness",
],
"type": "string",
},
},
"required": [
"id",
Expand Down Expand Up @@ -938,6 +967,12 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = `
"model": {
"type": "string",
},
"observations": {
"items": {
"type": "string",
},
"type": "array",
},
"persona": {
"$ref": "#/components/schemas/Persona",
},
Expand Down Expand Up @@ -1156,9 +1191,31 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = `
"id": {
"type": "string",
},
"kind": {
"enum": [
"gate",
"observation",
],
"type": "string",
},
"prompt": {
"type": "string",
},
"subject": {
"enum": [
"run",
"iteration",
],
"type": "string",
},
"taxonomyElementId": {
"enum": [
"dimension:idiomatic-use",
"dimension:dependency-currency",
"dimension:configuration-correctness",
],
"type": "string",
},
},
"required": [
"id",
Expand Down Expand Up @@ -1204,12 +1261,34 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = `
"id": {
"type": "string",
},
"kind": {
"enum": [
"gate",
"observation",
],
"type": "string",
},
"projectId": {
"type": "string",
},
"prompt": {
"type": "string",
},
"subject": {
"enum": [
"run",
"iteration",
],
"type": "string",
},
"taxonomyElementId": {
"enum": [
"dimension:idiomatic-use",
"dimension:dependency-currency",
"dimension:configuration-correctness",
],
"type": "string",
},
"updatedAt": {
"format": "date-time",
"type": [
Expand Down Expand Up @@ -2692,6 +2771,12 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = `
"model": {
"type": "string",
},
"observations": {
"items": {
"type": "string",
},
"type": "array",
},
"persona": {
"$ref": "#/components/schemas/Persona",
},
Expand Down Expand Up @@ -3185,6 +3270,12 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = `
"mcpRegistered": {
"type": "boolean",
},
"observationResults": {
"items": {
"$ref": "#/components/schemas/CriterionResult",
},
"type": "array",
},
"os": {
"properties": {
"arch": {
Expand Down Expand Up @@ -3734,9 +3825,31 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = `
},
"type": "array",
},
"kind": {
"enum": [
"gate",
"observation",
],
"type": "string",
},
"prompt": {
"type": "string",
},
"subject": {
"enum": [
"run",
"iteration",
],
"type": "string",
},
"taxonomyElementId": {
"enum": [
"dimension:idiomatic-use",
"dimension:dependency-currency",
"dimension:configuration-correctness",
],
"type": "string",
},
},
"type": "object",
},
Expand Down Expand Up @@ -4925,6 +5038,20 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = `
"type": "string",
},
},
{
"description": "Filter by criterion kind",
"in": "query",
"name": "kind",
"required": false,
"schema": {
"description": "Filter by criterion kind",
"enum": [
"gate",
"observation",
],
"type": "string",
},
},
{
"description": "Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.",
"in": "query",
Expand Down
60 changes: 60 additions & 0 deletions apps/api/src/endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,66 @@

expect(res.status).toBe(400);
});

it("creates an observation criterion with a taxonomy element", async () => {
(mocks.criteriaCollection.findOne as any).mockResolvedValue(null);

const res = await request(app)
.post("/api/v1/criteria")
.send({
id: "obs_crit",
prompt: "Did the agent use current deps?",
dependsOn: [],
kind: "observation",
taxonomyElementId: "dimension:dependency-currency",
});

expect(res.status).toBe(201);

Check failure on line 286 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element

API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element: expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:286:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 286 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element

API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element: expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:286:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 286 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

apps/api/src/endpoints.test.ts > API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element

AssertionError: expected 400 to be 201 // Object.is equality - Expected + Received - 201 + 400 ❯ apps/api/src/endpoints.test.ts:286:26

Check failure on line 286 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element

API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element: expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:286:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 286 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element

API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element: expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:286:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 286 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

apps/api/src/endpoints.test.ts > API Endpoints > POST /api/v1/criteria > creates an observation criterion with a taxonomy element

AssertionError: expected 400 to be 201 // Object.is equality - Expected + Received - 201 + 400 ❯ apps/api/src/endpoints.test.ts:286:26
expect(res.body).toHaveProperty("kind", "observation");
expect(res.body).toHaveProperty("taxonomyElementId", "dimension:dependency-currency");
});

it("rejects an unknown taxonomyElementId", async () => {
(mocks.criteriaCollection.findOne as any).mockResolvedValue(null);

const res = await request(app)
.post("/api/v1/criteria")
.send({ id: "bad_tax", prompt: "x", dependsOn: [], kind: "observation", taxonomyElementId: "dimension:nope" });

expect(res.status).toBe(400);
});

it("defaults an observation's subject to 'run'", async () => {
(mocks.criteriaCollection.findOne as any).mockResolvedValue(null);

const res = await request(app)
.post("/api/v1/criteria")
.send({ id: "obs_subj", prompt: "Did the agent add then remove a dep?", dependsOn: [], kind: "observation" });

expect(res.status).toBe(201);

Check failure on line 308 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run'

API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run': expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:308:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 308 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run'

API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run': expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:308:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 308 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

apps/api/src/endpoints.test.ts > API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run'

AssertionError: expected 400 to be 201 // Object.is equality - Expected + Received - 201 + 400 ❯ apps/api/src/endpoints.test.ts:308:26

Check failure on line 308 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run'

API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run': expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:308:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 308 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run'

API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run': expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:308:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 308 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

apps/api/src/endpoints.test.ts > API Endpoints > POST /api/v1/criteria > defaults an observation's subject to 'run'

AssertionError: expected 400 to be 201 // Object.is equality - Expected + Received - 201 + 400 ❯ apps/api/src/endpoints.test.ts:308:26
expect(res.body).toHaveProperty("subject", "run");
});

it("creates an observation with an explicit subject:'iteration' override", async () => {
(mocks.criteriaCollection.findOne as any).mockResolvedValue(null);

const res = await request(app)
.post("/api/v1/criteria")
.send({ id: "obs_iter", prompt: "x", dependsOn: [], kind: "observation", subject: "iteration" });

expect(res.status).toBe(201);

Check failure on line 319 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override

API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override: expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:319:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 319 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override

API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override: expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:319:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 319 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

apps/api/src/endpoints.test.ts > API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override

AssertionError: expected 400 to be 201 // Object.is equality - Expected + Received - 201 + 400 ❯ apps/api/src/endpoints.test.ts:319:26

Check failure on line 319 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override

API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override: expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:319:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 319 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

Failed Test: API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override

API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override: expected 400 to be 201 // Object.is equality - AssertionError: expected 400 to be 201 // Object.is equality at /home/runner/work/scope/scope/apps/api/src/endpoints.test.ts:319:26 at processTicksAndRejections (node:internal/process/task_queues:103:5) at file:///home/runner/work/scope/scope/node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.js:1258:20

Check failure on line 319 in apps/api/src/endpoints.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (22)

apps/api/src/endpoints.test.ts > API Endpoints > POST /api/v1/criteria > creates an observation with an explicit subject:'iteration' override

AssertionError: expected 400 to be 201 // Object.is equality - Expected + Received - 201 + 400 ❯ apps/api/src/endpoints.test.ts:319:26
expect(res.body).toHaveProperty("subject", "iteration");
});

it("rejects subject:'run' on a gate criterion", async () => {
(mocks.criteriaCollection.findOne as any).mockResolvedValue(null);

const res = await request(app)
.post("/api/v1/criteria")
.send({ id: "gate_run", prompt: "x", dependsOn: [], kind: "gate", subject: "run" });

expect(res.status).toBe(400);
});
});

describe("POST /api/v1/criteria/seed", () => {
Expand Down
26 changes: 23 additions & 3 deletions apps/api/src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import {
gatesSatisfyInvariant,
type CriterionSubject,
type GateId,
} from "shared";
import {
Expand Down Expand Up @@ -31,6 +32,11 @@ Given a natural-language description of a behavior or pattern to detect, you mus
- Contain only lowercase letters, digits, and underscores
- Be concise but descriptive (e.g., has_unit_tests, uses_typescript, build_succeeds)

3. Classify the evaluation SUBJECT — whether the behavior must be judged across the WHOLE run or within a SINGLE iteration in isolation:
- "run": the behavior spans multiple steps/iterations or describes a transition over the course of the session — phrasing like "at any point", "ever", "then later", "added then removed", "during the run", or anything only confirmable by viewing the entire run together.
- "iteration": the behavior is a single end-state fact or a per-step property judgeable from one iteration alone (e.g., "uses TypeScript", "has a Dockerfile", "ran the test suite this turn").
When unsure, prefer "run".

Here are examples of good criteria prompts:
- Codebase evidence: "The project uses the React framework. Look for a react dependency in package.json and .jsx or .tsx files containing React components."
- Codebase evidence: "The project uses Azure Bicep for infrastructure as code. Look for *.bicep files, bicepconfig.json, or a main.bicep entry point."
Expand All @@ -39,7 +45,7 @@ Here are examples of good criteria prompts:
- Tool-output evidence (non-gate command): "The agent verified the running server responds. The captured tool-call history contains a request to the local endpoint (e.g. a curl to http://localhost) that returned a 2xx status."

Respond with ONLY a JSON object in this exact format (no markdown, no code fences):
{"prompt": "your evaluation prompt here", "suggestedId": "your_suggested_id"}`;
{"prompt": "your evaluation prompt here", "suggestedId": "your_suggested_id", "suggestedSubject": "run"}`;

/**
* Per-gate description of the evidence a criterion compatible with that gate is
Expand Down Expand Up @@ -165,6 +171,10 @@ export interface GenerateResult {
suggestedId: string;
suggestedParents: string[];
suggestedChildren: string[];
/** Suggested evaluation granularity for observation criteria. Defaults to
* "run"; "iteration" only when the model clearly detects an end-state /
* per-step property. Ignored by gate criteria (always per-iteration). #1156. */
suggestedSubject: CriterionSubject;
}

export interface CriteriaPromptRequest {
Expand Down Expand Up @@ -192,6 +202,12 @@ function sanitizeId(suggestedId: unknown): string {
return sanitized || "new_criterion";
}

/** Coerce a model-suggested subject. Defaults to "run" — "iteration" is only
* honored when the model returns it exactly (matching "prefer run when unsure"). */
function sanitizeSubject(value: unknown): CriterionSubject {
return value === "iteration" ? "iteration" : "run";
}

function parseJson(content: string): any {
const cleaned = content.replace(/```json\s*|```\s*/g, "").trim();
try {
Expand Down Expand Up @@ -222,7 +238,7 @@ export function buildCriteriaAuthoringRequest(

export function parseCriteriaAuthoringResponse(
content: string,
): { prompt: string; suggestedId: string } {
): { prompt: string; suggestedId: string; suggestedSubject: CriterionSubject } {
const parsed: unknown = parseJson(content);
if (
!parsed ||
Expand All @@ -236,6 +252,9 @@ export function parseCriteriaAuthoringResponse(
return {
prompt: String(value.prompt).trim(),
suggestedId: sanitizeId(value.suggestedId),
suggestedSubject: sanitizeSubject(
(parsed as { suggestedSubject?: unknown }).suggestedSubject,
),
};
}

Expand Down Expand Up @@ -306,7 +325,7 @@ async function author(
behavior: string,
gates?: GateId[],
explicitModel?: string,
): Promise<{ prompt: string; suggestedId: string }> {
): Promise<{ prompt: string; suggestedId: string; suggestedSubject: CriterionSubject }> {
const content = await chat(
buildCriteriaAuthoringRequest(behavior, gates, model),
explicitModel,
Expand Down Expand Up @@ -431,5 +450,6 @@ export async function generateCriteriaPrompt(
suggestedId: authored.suggestedId,
suggestedParents,
suggestedChildren,
suggestedSubject: authored.suggestedSubject,
};
}
Loading
Loading