From 7935b73374e0cbd6ec315331cdb465c863438390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Thu, 28 May 2026 00:11:30 +0000 Subject: [PATCH] fix(util): remove legacy frontmatter fields next/confidence/artifacts/scope (#568) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 小橘 Co-committed-by: 小橘 --- .../__tests__/frontmatter-fast-path.test.ts | 115 ++++---- .../workflow-util-agent/src/frontmatter.ts | 21 +- .../__tests__/frontmatter-markdown.test.ts | 276 ++++-------------- .../frontmatter-markdown.ts | 73 +---- .../src/frontmatter-markdown/index.ts | 1 - .../src/frontmatter-markdown/types.ts | 54 +--- packages/workflow-util/src/index.ts | 1 - 7 files changed, 130 insertions(+), 411 deletions(-) diff --git a/packages/workflow-util-agent/__tests__/frontmatter-fast-path.test.ts b/packages/workflow-util-agent/__tests__/frontmatter-fast-path.test.ts index 69df302..c42f1dc 100644 --- a/packages/workflow-util-agent/__tests__/frontmatter-fast-path.test.ts +++ b/packages/workflow-util-agent/__tests__/frontmatter-fast-path.test.ts @@ -5,17 +5,13 @@ import { tryFrontmatterFastPath } from "../src/frontmatter.js"; // ── Helpers ─────────────────────────────────────────────────────────────────── -/** JSON Schema that exactly matches the AgentFrontmatter fields. */ -const FRONTMATTER_SCHEMA = { +/** JSON Schema that matches the new status-only AgentFrontmatter. */ +const STATUS_ONLY_SCHEMA = { type: "object", properties: { status: { anyOf: [{ type: "string" }, { type: "null" }] }, - next: { anyOf: [{ type: "string" }, { type: "null" }] }, - confidence: { anyOf: [{ type: "number" }, { type: "null" }] }, - artifacts: { type: "array", items: { type: "string" } }, - scope: { type: "string" }, }, - required: ["status", "next", "confidence", "artifacts", "scope"], + required: ["status"], additionalProperties: false, }; @@ -56,24 +52,41 @@ async function makeStoreWithSchema(schema: Record) { return { store, schemaHash }; } +// ── STANDARD_KEYS ──────────────────────────────────────────────────────────── + +describe("STANDARD_KEYS contains only status", () => { + test("STANDARD_KEYS is ['status']", async () => { + // We verify indirectly: defaultCandidate (no schema fields) returns only { status } + const { store, schemaHash } = await makeStoreWithSchema({ + type: "object", + properties: { + status: { anyOf: [{ type: "string" }, { type: "null" }] }, + }, + }); + + const raw = "---\nstatus: done\n---\n\nBody."; + const result = await tryFrontmatterFastPath(raw, schemaHash, store); + expect(result).not.toBeNull(); + + const node = store.get(result!.outputHash); + expect(node).not.toBeNull(); + const payload = node!.payload as Record; + expect(payload.status).toBe("done"); + // Legacy fields must NOT be present + expect(payload.next).toBeUndefined(); + expect(payload.confidence).toBeUndefined(); + expect(payload.artifacts).toBeUndefined(); + expect(payload.scope).toBeUndefined(); + }); +}); + // ── Happy path ───────────────────────────────────────────────────────────────── describe("tryFrontmatterFastPath — happy path", () => { test("parses valid frontmatter and returns outputHash + stripped body", async () => { - const { store, schemaHash } = await makeStoreWithSchema(FRONTMATTER_SCHEMA); + const { store, schemaHash } = await makeStoreWithSchema(STATUS_ONLY_SCHEMA); - const raw = [ - "---", - "status: done", - "next: reviewer", - "confidence: 0.9", - "artifacts: [src/foo.ts]", - "scope: role", - "---", - "", - "## Summary", - "Work is complete.", - ].join("\n"); + const raw = ["---", "status: done", "---", "", "## Summary", "Work is complete."].join("\n"); const result = await tryFrontmatterFastPath(raw, schemaHash, store); @@ -85,11 +98,10 @@ describe("tryFrontmatterFastPath — happy path", () => { expect((result?.outputHash ?? "").length).toBeGreaterThan(0); }); - test("stored CAS node payload matches frontmatter fields", async () => { - const { store, schemaHash } = await makeStoreWithSchema(FRONTMATTER_SCHEMA); + test("stored CAS node payload has only status", async () => { + const { store, schemaHash } = await makeStoreWithSchema(STATUS_ONLY_SCHEMA); - const raw = - "---\nstatus: done\nnext: null\nconfidence: null\nartifacts: []\nscope: role\n---\n\nBody."; + const raw = "---\nstatus: done\n---\n\nBody."; const result = await tryFrontmatterFastPath(raw, schemaHash, store); expect(result).not.toBeNull(); @@ -98,10 +110,29 @@ describe("tryFrontmatterFastPath — happy path", () => { expect(node).not.toBeNull(); const payload = node!.payload as Record; expect(payload.status).toBe("done"); - expect(payload.next).toBeNull(); - expect(payload.confidence).toBeNull(); - expect(payload.artifacts).toEqual([]); - expect(payload.scope).toBe("role"); + expect(Object.keys(payload)).toEqual(["status"]); + }); +}); + +// ── Legacy fields in input are ignored ────────────────────────────────────── + +describe("tryFrontmatterFastPath — legacy fields ignored", () => { + test("legacy fields in input do not appear in CAS output", async () => { + const { store, schemaHash } = await makeStoreWithSchema(STATUS_ONLY_SCHEMA); + + const raw = + "---\nstatus: done\nnext: reviewer\nconfidence: 0.9\nartifacts: [a.ts]\nscope: thread\n---\n\nBody."; + + const result = await tryFrontmatterFastPath(raw, schemaHash, store); + expect(result).not.toBeNull(); + + const node = store.get(result!.outputHash); + const payload = node!.payload as Record; + expect(payload.status).toBe("done"); + expect(payload.next).toBeUndefined(); + expect(payload.confidence).toBeUndefined(); + expect(payload.artifacts).toBeUndefined(); + expect(payload.scope).toBeUndefined(); }); }); @@ -109,7 +140,7 @@ describe("tryFrontmatterFastPath — happy path", () => { describe("tryFrontmatterFastPath — fallback: no frontmatter", () => { test("returns null for plain markdown without frontmatter block", async () => { - const { store, schemaHash } = await makeStoreWithSchema(FRONTMATTER_SCHEMA); + const { store, schemaHash } = await makeStoreWithSchema(STATUS_ONLY_SCHEMA); const result = await tryFrontmatterFastPath( "This is plain markdown without any frontmatter.", @@ -121,35 +152,13 @@ describe("tryFrontmatterFastPath — fallback: no frontmatter", () => { }); }); -// ── Fallback: invalid frontmatter ───────────────────────────────────────────── - -describe("tryFrontmatterFastPath — fallback: invalid frontmatter", () => { - test("returns null when confidence is out of range [0, 1]", async () => { - const { store, schemaHash } = await makeStoreWithSchema(FRONTMATTER_SCHEMA); - - const raw = "---\nstatus: done\nconfidence: 1.5\nscope: role\n---\n\nBody."; - - const result = await tryFrontmatterFastPath(raw, schemaHash, store); - expect(result).toBeNull(); - }); - - test("returns null when next contains whitespace", async () => { - const { store, schemaHash } = await makeStoreWithSchema(FRONTMATTER_SCHEMA); - - const raw = "---\nstatus: done\nnext: some role\nscope: role\n---\n\nBody."; - - const result = await tryFrontmatterFastPath(raw, schemaHash, store); - expect(result).toBeNull(); - }); -}); - // ── Fallback: schema mismatch ───────────────────────────────────────────────── describe("tryFrontmatterFastPath — fallback: schema mismatch", () => { test("returns null when outputSchema requires fields not in frontmatter", async () => { const { store, schemaHash } = await makeStoreWithSchema(STRICT_SCHEMA); - const raw = "---\nstatus: done\nscope: role\n---\n\nBody."; + const raw = "---\nstatus: done\n---\n\nBody."; const result = await tryFrontmatterFastPath(raw, schemaHash, store); expect(result).toBeNull(); @@ -194,7 +203,7 @@ describe("tryFrontmatterFastPath — role-specific fields", () => { test("returns null when required role-specific field is missing", async () => { const { store, schemaHash } = await makeStoreWithSchema(REVIEWER_SCHEMA); - const raw = "---\nstatus: done\nscope: role\n---\n\nBody."; + const raw = "---\nstatus: done\n---\n\nBody."; const result = await tryFrontmatterFastPath(raw, schemaHash, store); expect(result).toBeNull(); diff --git a/packages/workflow-util-agent/src/frontmatter.ts b/packages/workflow-util-agent/src/frontmatter.ts index 3e49666..3b926b3 100644 --- a/packages/workflow-util-agent/src/frontmatter.ts +++ b/packages/workflow-util-agent/src/frontmatter.ts @@ -13,7 +13,7 @@ import { extractSchemaFields } from "./build-output-format-instruction.js"; const log = createLogger({ sink: { kind: "stderr" } }); -const STANDARD_KEYS = ["status", "next", "confidence", "artifacts", "scope"] as const; +const STANDARD_KEYS = ["status"] as const; type StandardKey = (typeof STANDARD_KEYS)[number]; @@ -62,10 +62,6 @@ function parseRawFrontmatterFields(raw: string): Record { function defaultCandidate(frontmatter: AgentFrontmatter): Record { return { status: frontmatter.status, - next: frontmatter.next, - confidence: frontmatter.confidence, - artifacts: [...frontmatter.artifacts], - scope: frontmatter.scope, }; } @@ -73,14 +69,6 @@ function pickStandardField(frontmatter: AgentFrontmatter, key: StandardKey): unk switch (key) { case "status": return frontmatter.status; - case "next": - return frontmatter.next; - case "confidence": - return frontmatter.confidence; - case "artifacts": - return [...frontmatter.artifacts]; - case "scope": - return frontmatter.scope; } } @@ -98,9 +86,6 @@ function pickFieldValue( } const coerced = pickStandardField(frontmatter, field); - if (field === "artifacts" || field === "scope") { - return coerced; - } if (coerced !== null) { return coerced; } @@ -110,8 +95,8 @@ function pickFieldValue( /** * Build a CAS candidate object from schema property keys and parsed frontmatter. * - * When the schema has no inspectable properties, falls back to the five standard - * agent frontmatter fields for backward compatibility. + * When the schema has no inspectable properties, falls back to the standard + * agent frontmatter field (status only). */ function buildCandidate( frontmatter: AgentFrontmatter, diff --git a/packages/workflow-util/__tests__/frontmatter-markdown.test.ts b/packages/workflow-util/__tests__/frontmatter-markdown.test.ts index 8472df4..b12a4e1 100644 --- a/packages/workflow-util/__tests__/frontmatter-markdown.test.ts +++ b/packages/workflow-util/__tests__/frontmatter-markdown.test.ts @@ -41,31 +41,13 @@ describe("parseFrontmatterMarkdown", () => { }); }); - describe("full frontmatter document", () => { - it("parses all fields from a well-formed document", () => { - const raw = `--- -status: done -next: reviewer -confidence: 0.9 -artifacts: - - src/foo.ts - - src/bar.ts -scope: thread ---- - -## Summary - -Everything looks good.`; - + describe("status-only frontmatter", () => { + it("parses status-only frontmatter", () => { + const raw = "---\nstatus: done\n---\nbody"; const result = parseFrontmatterMarkdown(raw); expect(result.frontmatter).not.toBeNull(); - const fm = result.frontmatter!; - expect(fm.status).toBe("done"); - expect(fm.next).toBe("reviewer"); - expect(fm.confidence).toBe(0.9); - expect(fm.artifacts).toEqual(["src/foo.ts", "src/bar.ts"]); - expect(fm.scope).toBe("thread"); - expect(result.body).toBe("## Summary\n\nEverything looks good."); + expect(result.frontmatter).toEqual({ status: "done" }); + expect(result.body).toBe("body"); }); it("strips leading newline from body", () => { @@ -87,6 +69,22 @@ Everything looks good.`; }); }); + describe("ignores legacy fields", () => { + it("legacy fields next/confidence/artifacts/scope are NOT present on result", () => { + const raw = + "---\nstatus: done\nnext: reviewer\nconfidence: 0.9\nartifacts:\n - src/foo.ts\nscope: thread\n---\n\nBody."; + const result = parseFrontmatterMarkdown(raw); + expect(result.frontmatter).not.toBeNull(); + const fm = result.frontmatter!; + expect(fm.status).toBe("done"); + // Legacy fields must not exist on the object at all + expect("next" in fm).toBe(false); + expect("confidence" in fm).toBe(false); + expect("artifacts" in fm).toBe(false); + expect("scope" in fm).toBe(false); + }); + }); + describe("status field", () => { it.each([ "done", @@ -106,109 +104,18 @@ Everything looks good.`; }); it("returns null status when omitted", () => { - const raw = "---\nconfidence: 0.5\n---\nbody"; + const raw = "---\nfoo: bar\n---\nbody"; const result = parseFrontmatterMarkdown(raw); expect(result.frontmatter?.status).toBeNull(); }); }); - describe("confidence field", () => { - it("parses integer as number", () => { - const raw = "---\nconfidence: 1\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.confidence).toBe(1); - }); - - it("parses decimal", () => { - const raw = "---\nconfidence: 0.75\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.confidence).toBe(0.75); - }); - - it("returns null when omitted", () => { - const raw = "---\nstatus: done\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.confidence).toBeNull(); - }); - - it("returns null for non-numeric value", () => { - const raw = "---\nconfidence: high\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.confidence).toBeNull(); - }); - }); - - describe("artifacts field", () => { - it("parses block sequence", () => { - const raw = "---\nartifacts:\n - a.ts\n - b.ts\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.artifacts).toEqual(["a.ts", "b.ts"]); - }); - - it("parses inline sequence", () => { - const raw = "---\nartifacts: [a.ts, b.ts]\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.artifacts).toEqual(["a.ts", "b.ts"]); - }); - - it("returns empty array when omitted", () => { - const raw = "---\nstatus: done\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.artifacts).toEqual([]); - }); - - it("wraps single scalar in array", () => { - const raw = "---\nartifacts: only-one.ts\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.artifacts).toEqual(["only-one.ts"]); - }); - }); - - describe("scope field", () => { - it('parses scope "role"', () => { - const raw = "---\nscope: role\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.scope).toBe("role"); - }); - - it('parses scope "thread"', () => { - const raw = "---\nscope: thread\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.scope).toBe("thread"); - }); - - it('defaults to "role" when omitted', () => { - const raw = "---\nstatus: done\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.scope).toBe("role"); - }); - - it('defaults to "role" for unknown scope value', () => { - const raw = "---\nscope: global\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.scope).toBe("role"); - }); - }); - - describe("next field", () => { - it("parses a role name", () => { - const raw = "---\nnext: planner\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.next).toBe("planner"); - }); - - it("returns null when omitted", () => { - const raw = "---\nstatus: done\n---\nbody"; - const result = parseFrontmatterMarkdown(raw); - expect(result.frontmatter?.next).toBeNull(); - }); - }); - describe("unknown fields", () => { it("ignores unknown keys silently", () => { const raw = "---\nunknown_field: some_value\nstatus: done\n---\nbody"; const result = parseFrontmatterMarkdown(raw); expect(result.frontmatter?.status).toBe("done"); + expect(Object.keys(result.frontmatter!)).toEqual(["status"]); }); }); @@ -221,123 +128,58 @@ Everything looks good.`; }); describe("empty frontmatter block", () => { - it("parses empty frontmatter and uses all defaults", () => { + it("parses empty frontmatter with status null", () => { const raw = "---\n---\nbody"; const result = parseFrontmatterMarkdown(raw); expect(result.frontmatter).not.toBeNull(); const fm = result.frontmatter!; expect(fm.status).toBeNull(); - expect(fm.next).toBeNull(); - expect(fm.confidence).toBeNull(); - expect(fm.artifacts).toEqual([]); - expect(fm.scope).toBe("role"); + expect(Object.keys(fm)).toEqual(["status"]); expect(result.body).toBe("body"); }); }); + + describe("AgentFrontmatter has exactly one field", () => { + it("has only status key", () => { + const fm: AgentFrontmatter = { status: null }; + expect(Object.keys(fm)).toEqual(["status"]); + }); + }); + + describe("FrontmatterValidationError only has status variant", () => { + it("status variant is valid", () => { + const err: import("../src/index.js").FrontmatterValidationError = { + field: "status", + message: "test", + }; + expect(err.field).toBe("status"); + }); + }); }); // ── validateFrontmatter ────────────────────────────────────────────────────── -function validFm(overrides: Partial = {}): AgentFrontmatter { - return { - status: "done", - next: null, - confidence: null, - artifacts: [], - scope: "role", - ...overrides, - }; -} - describe("validateFrontmatter", () => { - it("returns no errors for a fully valid frontmatter", () => { - const errors = validateFrontmatter(validFm()); + it("returns no errors for a valid status", () => { + const errors = validateFrontmatter({ status: "done" }); expect(errors).toHaveLength(0); }); - it("returns no errors when all nullable fields are null", () => { - const fm: AgentFrontmatter = { - status: null, - next: null, - confidence: null, - artifacts: [], - scope: "role", - }; + it("returns no errors when status is null", () => { + const errors = validateFrontmatter({ status: null }); + expect(errors).toHaveLength(0); + }); + + it("returns error for invalid status", () => { + const errors = validateFrontmatter({ status: "bogus" as never }); + expect(errors).toHaveLength(1); + expect(errors[0]?.field).toBe("status"); + }); + + it("no validation for next/confidence/artifacts/scope — fields do not exist", () => { + // AgentFrontmatter only has status — verify at runtime + const fm: AgentFrontmatter = { status: "done" }; + expect(Object.keys(fm)).toEqual(["status"]); expect(validateFrontmatter(fm)).toHaveLength(0); }); - - describe("confidence validation", () => { - it("accepts 0.0", () => { - expect(validateFrontmatter(validFm({ confidence: 0 }))).toHaveLength(0); - }); - - it("accepts 1.0", () => { - expect(validateFrontmatter(validFm({ confidence: 1 }))).toHaveLength(0); - }); - - it("rejects value below 0", () => { - const errors = validateFrontmatter(validFm({ confidence: -0.1 })); - expect(errors).toHaveLength(1); - expect(errors[0]?.field).toBe("confidence"); - }); - - it("rejects value above 1", () => { - const errors = validateFrontmatter(validFm({ confidence: 1.01 })); - expect(errors).toHaveLength(1); - expect(errors[0]?.field).toBe("confidence"); - }); - }); - - describe("next validation", () => { - it("accepts a simple role name", () => { - expect(validateFrontmatter(validFm({ next: "reviewer" }))).toHaveLength(0); - }); - - it("accepts kebab-case role name", () => { - expect(validateFrontmatter(validFm({ next: "code-reviewer" }))).toHaveLength(0); - }); - - it("rejects role name with whitespace", () => { - const errors = validateFrontmatter(validFm({ next: "role name" })); - expect(errors).toHaveLength(1); - expect(errors[0]?.field).toBe("next"); - }); - }); - - describe("artifacts validation", () => { - it("accepts non-empty path strings", () => { - expect( - validateFrontmatter(validFm({ artifacts: ["src/foo.ts", "src/bar.ts"] })), - ).toHaveLength(0); - }); - - it("rejects empty string artifact entries", () => { - const errors = validateFrontmatter(validFm({ artifacts: [""] })); - expect(errors).toHaveLength(1); - expect(errors[0]?.field).toBe("artifacts"); - }); - - it("rejects whitespace-only artifact entries", () => { - const errors = validateFrontmatter(validFm({ artifacts: [" "] })); - expect(errors).toHaveLength(1); - expect(errors[0]?.field).toBe("artifacts"); - }); - }); - - describe("multiple errors", () => { - it("reports multiple violations at once", () => { - const fm: AgentFrontmatter = { - status: "done", - next: "bad role", - confidence: 2, - artifacts: [""], - scope: "role", - }; - const errors = validateFrontmatter(fm); - const fields = errors.map((e) => e.field); - expect(fields).toContain("next"); - expect(fields).toContain("confidence"); - expect(fields).toContain("artifacts"); - }); - }); }); diff --git a/packages/workflow-util/src/frontmatter-markdown/frontmatter-markdown.ts b/packages/workflow-util/src/frontmatter-markdown/frontmatter-markdown.ts index ae92108..e787c16 100644 --- a/packages/workflow-util/src/frontmatter-markdown/frontmatter-markdown.ts +++ b/packages/workflow-util/src/frontmatter-markdown/frontmatter-markdown.ts @@ -1,6 +1,5 @@ import type { AgentFrontmatter, - FrontmatterScope, FrontmatterStatus, FrontmatterValidationError, ParsedFrontmatterMarkdown, @@ -159,40 +158,12 @@ function parseMinimalYaml(yaml: string): Record { const VALID_STATUS: readonly FrontmatterStatus[] = ["done", "needs_input", "in_progress", "failed"]; -const VALID_SCOPE: readonly FrontmatterScope[] = ["role", "thread"]; - function coerceStatus(raw: YamlValue): FrontmatterStatus | null { if (raw === null || raw === undefined) return null; const s = String(raw).trim().toLowerCase(); return VALID_STATUS.includes(s as FrontmatterStatus) ? (s as FrontmatterStatus) : null; } -function coerceNext(raw: YamlValue): string | null { - if (raw === null || raw === undefined) return null; - const s = String(raw).trim(); - return s === "" ? null : s; -} - -function coerceConfidence(raw: YamlValue): number | null { - if (raw === null || raw === undefined) return null; - const n = typeof raw === "number" ? raw : Number(String(raw).trim()); - if (Number.isNaN(n)) return null; - return n; -} - -function coerceArtifacts(raw: YamlValue): readonly string[] { - if (raw === null || raw === undefined) return []; - if (Array.isArray(raw)) return raw.map(String).filter((s) => s !== ""); - const s = String(raw).trim(); - return s === "" ? [] : [s]; -} - -function coerceScope(raw: YamlValue): FrontmatterScope { - if (raw === null || raw === undefined) return "role"; - const s = String(raw).trim().toLowerCase(); - return VALID_SCOPE.includes(s as FrontmatterScope) ? (s as FrontmatterScope) : "role"; -} - // ── Public API ─────────────────────────────────────────────────────────────── /** @@ -220,10 +191,6 @@ export function parseFrontmatterMarkdown(raw: string): ParsedFrontmatterMarkdown const frontmatter: AgentFrontmatter = { status: coerceStatus(fields.status ?? null), - next: coerceNext(fields.next ?? null), - confidence: coerceConfidence(fields.confidence ?? null), - artifacts: coerceArtifacts(fields.artifacts ?? null), - scope: coerceScope(fields.scope ?? null), }; return { frontmatter, body }; @@ -235,11 +202,7 @@ export function parseFrontmatterMarkdown(raw: string): ParsedFrontmatterMarkdown * An empty array means the frontmatter is valid. * * Validated constraints: - * - `status` — must be one of the FrontmatterStatus literals (if non-null) - * - `confidence` — must be in [0.0, 1.0] (if non-null) - * - `next` — must be a non-empty string with no whitespace (if non-null) - * - `artifacts` — each entry must be a non-empty string - * - `scope` — must be one of the FrontmatterScope literals + * - `status` — must be one of the FrontmatterStatus literals (if non-null) */ export function validateFrontmatter( frontmatter: AgentFrontmatter, @@ -253,39 +216,5 @@ export function validateFrontmatter( }); } - if (frontmatter.confidence !== null) { - if (frontmatter.confidence < 0 || frontmatter.confidence > 1) { - errors.push({ - field: "confidence", - message: `confidence ${frontmatter.confidence} is out of range; must be between 0.0 and 1.0 inclusive`, - }); - } - } - - if (frontmatter.next !== null) { - if (frontmatter.next.trim() === "") { - errors.push({ field: "next", message: "next must be a non-empty string when present" }); - } else if (/\s/.test(frontmatter.next)) { - errors.push({ - field: "next", - message: `next "${frontmatter.next}" must not contain whitespace`, - }); - } - } - - for (const artifact of frontmatter.artifacts) { - if (artifact.trim() === "") { - errors.push({ field: "artifacts", message: "artifact entries must be non-empty strings" }); - break; - } - } - - if (!VALID_SCOPE.includes(frontmatter.scope)) { - errors.push({ - field: "scope", - message: `invalid scope "${frontmatter.scope}"; must be one of: ${VALID_SCOPE.join(", ")}`, - }); - } - return errors; } diff --git a/packages/workflow-util/src/frontmatter-markdown/index.ts b/packages/workflow-util/src/frontmatter-markdown/index.ts index eb1f826..bb6b11c 100644 --- a/packages/workflow-util/src/frontmatter-markdown/index.ts +++ b/packages/workflow-util/src/frontmatter-markdown/index.ts @@ -1,7 +1,6 @@ export { parseFrontmatterMarkdown, validateFrontmatter } from "./frontmatter-markdown.js"; export type { AgentFrontmatter, - FrontmatterScope, FrontmatterStatus, FrontmatterValidationError, ParsedFrontmatterMarkdown, diff --git a/packages/workflow-util/src/frontmatter-markdown/types.ts b/packages/workflow-util/src/frontmatter-markdown/types.ts index 439ed38..639a8cc 100644 --- a/packages/workflow-util/src/frontmatter-markdown/types.ts +++ b/packages/workflow-util/src/frontmatter-markdown/types.ts @@ -1,5 +1,5 @@ /** - * Frontmatter Markdown — agent output format (RFC #351 Phase 1). + * Frontmatter Markdown — agent output format. * * An agent response is a Markdown document with an optional YAML frontmatter * block at the top. The frontmatter carries structured signals that the @@ -9,17 +9,12 @@ * * --- * status: done - * next: reviewer - * confidence: 0.9 - * artifacts: - * - src/foo.ts - * scope: role * --- * * ... free-form markdown body ... * - * All frontmatter fields are optional at the parse level. `validateFrontmatter` - * enforces the constraints documented on each field below. + * Only `status` is a standard frontmatter field. All other fields are + * role-specific and defined by the output schema. */ // ── Vocabulary types ───────────────────────────────────────────────────────── @@ -34,20 +29,12 @@ */ export type FrontmatterStatus = "done" | "needs_input" | "in_progress" | "failed"; -/** - * Scope of frontmatter signals. - * - * - `role` — signals apply to the current role execution only (default) - * - `thread` — signals are suggestions for the entire thread moderator - */ -export type FrontmatterScope = "role" | "thread"; - // ── Core frontmatter schema ────────────────────────────────────────────────── /** * Parsed and validated frontmatter from an agent response. * - * All fields use explicit `T | null` (no optional `?:` per convention). + * Only `status` is a standard field. All other fields are role-specific. */ export type AgentFrontmatter = { /** @@ -55,32 +42,6 @@ export type AgentFrontmatter = { * Null when omitted — engine treats it as "done" for backward compatibility. */ status: FrontmatterStatus | null; - - /** - * Suggested next role name for the moderator. - * The moderator is NOT obligated to follow this — it is advisory only. - * Null when the agent has no preference. - */ - next: string | null; - - /** - * Agent's self-assessed confidence in its output (0.0 – 1.0 inclusive). - * Null when omitted. - */ - confidence: number | null; - - /** - * Relative file paths or CAS hashes the agent considers its primary outputs. - * Used for GC ref-tracing and human-readable summaries. - * Empty array when omitted (never null — an absent list is an empty list). - */ - artifacts: readonly string[]; - - /** - * Scope of the frontmatter signals. - * Defaults to "role" when omitted. - */ - scope: FrontmatterScope; }; // ── Parse output ───────────────────────────────────────────────────────────── @@ -103,9 +64,4 @@ export type ParsedFrontmatterMarkdown = { // ── Validation error ───────────────────────────────────────────────────────── -export type FrontmatterValidationError = - | { field: "status"; message: string } - | { field: "next"; message: string } - | { field: "confidence"; message: string } - | { field: "artifacts"; message: string } - | { field: "scope"; message: string }; +export type FrontmatterValidationError = { field: "status"; message: string }; diff --git a/packages/workflow-util/src/index.ts b/packages/workflow-util/src/index.ts index 393a416..6711b61 100644 --- a/packages/workflow-util/src/index.ts +++ b/packages/workflow-util/src/index.ts @@ -8,7 +8,6 @@ export { generateDeveloperReference } from "./developer-reference.js"; export { env } from "./env.js"; export type { AgentFrontmatter, - FrontmatterScope, FrontmatterStatus, FrontmatterValidationError, ParsedFrontmatterMarkdown,