orchestrator-workflow 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Structural schema for the reviewer output contract (the final ```yaml
3
+ * block in `assets/agents/reviewer.md`, mirrored byte-for-byte in
4
+ * `assets/skill/references/contracts.md`). This module is the single
5
+ * hand-maintained copy of that shape in code; `test/docs-consistency.test.ts`
6
+ * parses the contract block itself and asserts it matches the constants
7
+ * below exactly, so a contract edit without a matching edit here fails the
8
+ * suite instead of drifting silently.
9
+ *
10
+ * This validator checks structure only. It never judges semantic adequacy,
11
+ * never waives a finding, and its passing is never orchestrator acceptance.
12
+ */
13
+ /** Top-level field names, in the contract's own order. */
14
+ export declare const TOP_LEVEL_FIELDS: readonly ["status", "role", "task_id", "summary", "findings", "acceptance_recommendation", "missing_tests", "residual_risks", "reproduction", "method_applied", "withdrawn"];
15
+ /** Field names of one `findings[]` entry, in the contract's own order. */
16
+ export declare const FINDING_FIELDS: readonly ["severity", "category", "description", "suggested_fix", "recurrence", "introduced_by_delta"];
17
+ /** Field names of the `reproduction` object, in the contract's own order. */
18
+ export declare const REPRODUCTION_FIELDS: readonly ["method", "sample_size", "result", "matches_implementer_claim"];
19
+ /** Field names of one `withdrawn[]` entry, in the contract's own order. */
20
+ export declare const WITHDRAWN_FIELDS: readonly ["description", "reason"];
21
+ /**
22
+ * Enum spellings keyed by their bare field name. Every enum-bearing field
23
+ * in the contract has a name unique across the whole block, so a flat map
24
+ * (rather than one keyed by full path) is enough and matches how the
25
+ * contract text itself reads (`key: a | b | c`).
26
+ */
27
+ export declare const ENUM_VALUES: Readonly<Record<string, readonly string[]>>;
28
+ export interface Diagnostic {
29
+ /** Dotted/bracketed location of the offending field, e.g. `findings[0].severity`. */
30
+ path: string;
31
+ /** What the schema requires at that path. */
32
+ expected: string;
33
+ /** What was actually found (or "missing"). */
34
+ got: string;
35
+ }
36
+ export interface ValidationResult {
37
+ valid: boolean;
38
+ diagnostics: Diagnostic[];
39
+ /** Non-fatal observations, e.g. prose found after the fenced block. */
40
+ warnings: string[];
41
+ }
42
+ /**
43
+ * This validator reports structural validity only. It does not judge
44
+ * semantic adequacy, cannot waive findings, and passing it does not
45
+ * constitute orchestrator acceptance.
46
+ */
47
+ export declare const STRUCTURAL_ONLY_NOTE = "Structural check only: this does not judge semantic adequacy, cannot waive findings, and does not constitute orchestrator acceptance.";
48
+ /** Every field name any of the four contract constants declares. */
49
+ export type SchemaFieldName = (typeof TOP_LEVEL_FIELDS)[number] | (typeof FINDING_FIELDS)[number] | (typeof REPRODUCTION_FIELDS)[number] | (typeof WITHDRAWN_FIELDS)[number];
50
+ /**
51
+ * The structural kind of one contract field: which input classes its
52
+ * checker accepts and which it rejects. Declared once, next to the
53
+ * dispatch tables above, and read by both this module (for the `expected`
54
+ * text a diagnostic carries) and `test/review-report.test.ts`'s case
55
+ * generator, which derives its whole case list from these kinds rather
56
+ * than from a hand-enumerated list of checks.
57
+ *
58
+ * - `enum`: present, a string, and one of `ENUM_VALUES[field]`.
59
+ * - `string`: present and a string; the empty string is accepted.
60
+ * - `non-empty-string`: present, a string, and not blank.
61
+ * - `scalar`: present and either a string or a number.
62
+ * - `array`: present and an array; an empty array is accepted.
63
+ * - `mapping-list`: `array`, and every element a mapping.
64
+ * - `mapping`: present and a mapping.
65
+ *
66
+ * A new kind is declared here, in {@link KIND_EXPECTED}, and in the
67
+ * generator's own `Record<FieldKind, ...>` value tables; each of those is
68
+ * keyed by this type, so a kind with no expectation text or no input
69
+ * classes is a compile error rather than an untested kind.
70
+ */
71
+ export type FieldKind = "enum" | "string" | "non-empty-string" | "scalar" | "array" | "mapping-list" | "mapping";
72
+ /**
73
+ * The declared kind of every field of every contract constant, keyed by
74
+ * bare field name the way {@link ENUM_VALUES} is: a name is unique across
75
+ * the whole contract block except for `description`, which `findings[]`
76
+ * and `withdrawn[]` share with the same kind. The `satisfies
77
+ * Record<SchemaFieldName, FieldKind>` clause means a name added to any of
78
+ * the four constants without a kind here is a TypeScript compile error,
79
+ * the same way it is already an error to add one without a checker in the
80
+ * dispatch tables above. Were a future contract to reuse one name at two
81
+ * levels with two different kinds, this flat map could hold only one of
82
+ * them: the generator asserts each field's real diagnostics against its
83
+ * declared kind, so that shows up as a failing generated case rather than
84
+ * as an unchecked field.
85
+ */
86
+ export declare const FIELD_KINDS: {
87
+ status: "enum";
88
+ role: "enum";
89
+ task_id: "non-empty-string";
90
+ summary: "array";
91
+ findings: "mapping-list";
92
+ acceptance_recommendation: "enum";
93
+ missing_tests: "array";
94
+ residual_risks: "array";
95
+ reproduction: "mapping";
96
+ method_applied: "enum";
97
+ withdrawn: "mapping-list";
98
+ severity: "enum";
99
+ category: "enum";
100
+ description: "string";
101
+ suggested_fix: "string";
102
+ recurrence: "enum";
103
+ introduced_by_delta: "enum";
104
+ method: "scalar";
105
+ sample_size: "scalar";
106
+ result: "scalar";
107
+ matches_implementer_claim: "enum";
108
+ reason: "string";
109
+ };
110
+ /**
111
+ * The `expected` text this validator's diagnostics about `field` carry.
112
+ * The checkers above hold that text literally, at their own push sites;
113
+ * this is the schema's declaration of the same text, which the generated
114
+ * test cases assert the checkers actually produce, so a checker whose
115
+ * behaviour stops matching its declared kind fails a case instead of
116
+ * drifting quietly.
117
+ */
118
+ export declare function expectedTextFor(field: SchemaFieldName): string;
119
+ /** The `expected` text a diagnostic about one element of a `mapping-list` carries. */
120
+ export declare const MAPPING_LIST_ELEMENT_EXPECTED: string;
121
+ interface ExtractedYaml {
122
+ yamlText: string;
123
+ warnings: string[];
124
+ }
125
+ /**
126
+ * A reviewer return is commonly wrapped in a single fenced code block.
127
+ * Strips one leading/trailing fence when present, whatever language tag
128
+ * it carries (```yaml, ```yml, a bare ``` , or any other tag -- a
129
+ * language-less fence previously fell through to the "literal YAML"
130
+ * branch below and produced a confusing YAML parse error instead of a
131
+ * clean structural diagnostic; fix-round, review finding L2); anything
132
+ * unfenced is treated as literal YAML. The fence is located anywhere in
133
+ * the input, not only at its very start: a return prefixed with prose
134
+ * ("Here is my report:\n```yaml ...") previously fell through to the
135
+ * "literal YAML" branch, since the fence pattern was anchored to the
136
+ * start of the string, and produced a raw parse error instead of a
137
+ * structural diagnostic (fix-round, review finding L1). Prose found
138
+ * before the opening fence or after the closing fence is tolerated, but
139
+ * each is named as its own warning rather than silently dropped.
140
+ *
141
+ * The closing fence must start at column 0: the pattern anchors it with
142
+ * `^` under the `m` flag, so a triple-backtick sequence inside a value
143
+ * (a reviewer quoting a fenced snippet in a `description` block scalar,
144
+ * which YAML necessarily indents) can no longer close the block early
145
+ * and hand the parser a truncated document, which surfaced as
146
+ * diagnostics about fields the return actually carried (fix-round,
147
+ * review finding L3).
148
+ */
149
+ export declare function extractYamlSource(raw: string): ExtractedYaml;
150
+ /**
151
+ * Validates a reviewer return against the reviewer output contract's
152
+ * structure. Reports structural validity ONLY: it never judges semantic
153
+ * adequacy, never waives a finding, and passing it is never orchestrator
154
+ * acceptance.
155
+ */
156
+ export declare function validateReviewReport(raw: string): ValidationResult;
157
+ export {};
@@ -0,0 +1,449 @@
1
+ import { parse as parseYaml } from "yaml";
2
+ /**
3
+ * Structural schema for the reviewer output contract (the final ```yaml
4
+ * block in `assets/agents/reviewer.md`, mirrored byte-for-byte in
5
+ * `assets/skill/references/contracts.md`). This module is the single
6
+ * hand-maintained copy of that shape in code; `test/docs-consistency.test.ts`
7
+ * parses the contract block itself and asserts it matches the constants
8
+ * below exactly, so a contract edit without a matching edit here fails the
9
+ * suite instead of drifting silently.
10
+ *
11
+ * This validator checks structure only. It never judges semantic adequacy,
12
+ * never waives a finding, and its passing is never orchestrator acceptance.
13
+ */
14
+ /** Top-level field names, in the contract's own order. */
15
+ export const TOP_LEVEL_FIELDS = [
16
+ "status",
17
+ "role",
18
+ "task_id",
19
+ "summary",
20
+ "findings",
21
+ "acceptance_recommendation",
22
+ "missing_tests",
23
+ "residual_risks",
24
+ "reproduction",
25
+ "method_applied",
26
+ "withdrawn",
27
+ ];
28
+ /** Field names of one `findings[]` entry, in the contract's own order. */
29
+ export const FINDING_FIELDS = [
30
+ "severity",
31
+ "category",
32
+ "description",
33
+ "suggested_fix",
34
+ "recurrence",
35
+ "introduced_by_delta",
36
+ ];
37
+ /** Field names of the `reproduction` object, in the contract's own order. */
38
+ export const REPRODUCTION_FIELDS = [
39
+ "method",
40
+ "sample_size",
41
+ "result",
42
+ "matches_implementer_claim",
43
+ ];
44
+ /** Field names of one `withdrawn[]` entry, in the contract's own order. */
45
+ export const WITHDRAWN_FIELDS = ["description", "reason"];
46
+ /**
47
+ * Enum spellings keyed by their bare field name. Every enum-bearing field
48
+ * in the contract has a name unique across the whole block, so a flat map
49
+ * (rather than one keyed by full path) is enough and matches how the
50
+ * contract text itself reads (`key: a | b | c`).
51
+ */
52
+ export const ENUM_VALUES = {
53
+ status: ["reviewed"],
54
+ role: ["reviewer"],
55
+ severity: ["low", "medium", "high", "critical"],
56
+ category: [
57
+ "correctness",
58
+ "architecture",
59
+ "security",
60
+ "tests",
61
+ "maintainability",
62
+ "performance",
63
+ "docs",
64
+ ],
65
+ recurrence: ["new", "repeated"],
66
+ introduced_by_delta: ["yes", "no", "unknown"],
67
+ acceptance_recommendation: [
68
+ "accept",
69
+ "accept_with_notes",
70
+ "fix_required",
71
+ "reject",
72
+ ],
73
+ matches_implementer_claim: ["matched", "mismatched", "not_applicable"],
74
+ method_applied: ["normal", "rigorous", "adversarial"],
75
+ };
76
+ /**
77
+ * This validator reports structural validity only. It does not judge
78
+ * semantic adequacy, cannot waive findings, and passing it does not
79
+ * constitute orchestrator acceptance.
80
+ */
81
+ export const STRUCTURAL_ONLY_NOTE = "Structural check only: this does not judge semantic adequacy, cannot waive findings, and does not constitute orchestrator acceptance.";
82
+ function describeValue(value) {
83
+ if (value === undefined)
84
+ return "missing";
85
+ if (value === null)
86
+ return "null";
87
+ if (Array.isArray(value))
88
+ return `array(length=${value.length})`;
89
+ if (typeof value === "object")
90
+ return "mapping";
91
+ if (typeof value === "string")
92
+ return JSON.stringify(value);
93
+ return String(value);
94
+ }
95
+ function isPlainRecord(value) {
96
+ return typeof value === "object" && value !== null && !Array.isArray(value);
97
+ }
98
+ function checkEnumField(record, key, allowed, path, diagnostics) {
99
+ const value = record[key];
100
+ const expected = allowed.join(" | ");
101
+ if (value === undefined) {
102
+ diagnostics.push({ path, expected, got: "missing" });
103
+ return;
104
+ }
105
+ if (typeof value !== "string" || !allowed.includes(value)) {
106
+ diagnostics.push({ path, expected, got: describeValue(value) });
107
+ }
108
+ }
109
+ function checkStringField(record, key, path, diagnostics) {
110
+ const value = record[key];
111
+ if (value === undefined) {
112
+ diagnostics.push({ path, expected: "string", got: "missing" });
113
+ return;
114
+ }
115
+ if (typeof value !== "string") {
116
+ diagnostics.push({ path, expected: "string", got: describeValue(value) });
117
+ }
118
+ }
119
+ function checkNonEmptyStringField(record, key, path, diagnostics) {
120
+ const value = record[key];
121
+ if (value === undefined) {
122
+ diagnostics.push({ path, expected: "non-empty string", got: "missing" });
123
+ return;
124
+ }
125
+ if (typeof value !== "string" || value.trim().length === 0) {
126
+ diagnostics.push({
127
+ path,
128
+ expected: "non-empty string",
129
+ got: describeValue(value),
130
+ });
131
+ }
132
+ }
133
+ /** Accepts a string or a number (a reviewer may write `sample_size: 5`). */
134
+ function checkScalarField(record, key, path, diagnostics) {
135
+ const value = record[key];
136
+ if (value === undefined) {
137
+ diagnostics.push({ path, expected: "string or number", got: "missing" });
138
+ return;
139
+ }
140
+ if (typeof value !== "string" && typeof value !== "number") {
141
+ diagnostics.push({
142
+ path,
143
+ expected: "string or number",
144
+ got: describeValue(value),
145
+ });
146
+ }
147
+ }
148
+ function checkArrayField(doc, key, diagnostics) {
149
+ const value = doc[key];
150
+ if (value === undefined) {
151
+ diagnostics.push({ path: key, expected: "array", got: "missing" });
152
+ return;
153
+ }
154
+ if (!Array.isArray(value)) {
155
+ diagnostics.push({
156
+ path: key,
157
+ expected: "array",
158
+ got: describeValue(value),
159
+ });
160
+ }
161
+ }
162
+ /**
163
+ * Dispatch table keyed by every name in {@link FINDING_FIELDS}. The
164
+ * `Record<(typeof FINDING_FIELDS)[number], FieldChecker>` type means a
165
+ * name added to `FINDING_FIELDS` without a matching entry here is a
166
+ * TypeScript compile error, not a silently-unchecked field (fix-round,
167
+ * review finding M2).
168
+ */
169
+ const FINDING_CHECKS = {
170
+ severity: (entry, path, diagnostics) => checkEnumField(entry, "severity", ENUM_VALUES.severity, `${path}.severity`, diagnostics),
171
+ category: (entry, path, diagnostics) => checkEnumField(entry, "category", ENUM_VALUES.category, `${path}.category`, diagnostics),
172
+ description: (entry, path, diagnostics) => checkStringField(entry, "description", `${path}.description`, diagnostics),
173
+ suggested_fix: (entry, path, diagnostics) => checkStringField(entry, "suggested_fix", `${path}.suggested_fix`, diagnostics),
174
+ recurrence: (entry, path, diagnostics) => checkEnumField(entry, "recurrence", ENUM_VALUES.recurrence, `${path}.recurrence`, diagnostics),
175
+ introduced_by_delta: (entry, path, diagnostics) => checkEnumField(entry, "introduced_by_delta", ENUM_VALUES.introduced_by_delta, `${path}.introduced_by_delta`, diagnostics),
176
+ };
177
+ /** Dispatch table keyed by every name in {@link REPRODUCTION_FIELDS}. */
178
+ const REPRODUCTION_CHECKS = {
179
+ method: (value, path, diagnostics) => checkScalarField(value, "method", `${path}.method`, diagnostics),
180
+ sample_size: (value, path, diagnostics) => checkScalarField(value, "sample_size", `${path}.sample_size`, diagnostics),
181
+ result: (value, path, diagnostics) => checkScalarField(value, "result", `${path}.result`, diagnostics),
182
+ matches_implementer_claim: (value, path, diagnostics) => checkEnumField(value, "matches_implementer_claim", ENUM_VALUES.matches_implementer_claim, `${path}.matches_implementer_claim`, diagnostics),
183
+ };
184
+ /** Dispatch table keyed by every name in {@link WITHDRAWN_FIELDS}. */
185
+ const WITHDRAWN_CHECKS = {
186
+ description: (entry, path, diagnostics) => checkStringField(entry, "description", `${path}.description`, diagnostics),
187
+ reason: (entry, path, diagnostics) => checkStringField(entry, "reason", `${path}.reason`, diagnostics),
188
+ };
189
+ function checkFindings(doc, diagnostics) {
190
+ const value = doc.findings;
191
+ if (value === undefined) {
192
+ diagnostics.push({ path: "findings", expected: "array", got: "missing" });
193
+ return;
194
+ }
195
+ if (!Array.isArray(value)) {
196
+ diagnostics.push({
197
+ path: "findings",
198
+ expected: "array",
199
+ got: describeValue(value),
200
+ });
201
+ return;
202
+ }
203
+ value.forEach((entry, index) => {
204
+ const path = `findings[${index}]`;
205
+ if (!isPlainRecord(entry)) {
206
+ diagnostics.push({
207
+ path,
208
+ expected: "mapping",
209
+ got: describeValue(entry),
210
+ });
211
+ return;
212
+ }
213
+ for (const field of FINDING_FIELDS) {
214
+ FINDING_CHECKS[field](entry, path, diagnostics);
215
+ }
216
+ });
217
+ }
218
+ function checkReproduction(doc, diagnostics) {
219
+ const value = doc.reproduction;
220
+ if (value === undefined) {
221
+ diagnostics.push({
222
+ path: "reproduction",
223
+ expected: "mapping",
224
+ got: "missing",
225
+ });
226
+ return;
227
+ }
228
+ if (!isPlainRecord(value)) {
229
+ diagnostics.push({
230
+ path: "reproduction",
231
+ expected: "mapping",
232
+ got: describeValue(value),
233
+ });
234
+ return;
235
+ }
236
+ for (const field of REPRODUCTION_FIELDS) {
237
+ REPRODUCTION_CHECKS[field](value, "reproduction", diagnostics);
238
+ }
239
+ }
240
+ function checkWithdrawn(doc, diagnostics) {
241
+ const value = doc.withdrawn;
242
+ if (value === undefined) {
243
+ diagnostics.push({
244
+ path: "withdrawn",
245
+ expected: "array (may be empty)",
246
+ got: "missing",
247
+ });
248
+ return;
249
+ }
250
+ if (!Array.isArray(value)) {
251
+ diagnostics.push({
252
+ path: "withdrawn",
253
+ expected: "array (may be empty)",
254
+ got: describeValue(value),
255
+ });
256
+ return;
257
+ }
258
+ value.forEach((entry, index) => {
259
+ const path = `withdrawn[${index}]`;
260
+ if (!isPlainRecord(entry)) {
261
+ diagnostics.push({
262
+ path,
263
+ expected: "mapping",
264
+ got: describeValue(entry),
265
+ });
266
+ return;
267
+ }
268
+ for (const field of WITHDRAWN_FIELDS) {
269
+ WITHDRAWN_CHECKS[field](entry, path, diagnostics);
270
+ }
271
+ });
272
+ }
273
+ /**
274
+ * Dispatch table keyed by every name in {@link TOP_LEVEL_FIELDS}. As with
275
+ * {@link FINDING_CHECKS}, the `Record<(typeof TOP_LEVEL_FIELDS)[number],
276
+ * DocChecker>` type turns a `TOP_LEVEL_FIELDS` entry without a matching
277
+ * checker into a TypeScript compile error (fix-round, review finding M2):
278
+ * a field added to both the contract fences and this array can no longer
279
+ * go unchecked while every test stays green, since it fails to typecheck
280
+ * before any test runs.
281
+ */
282
+ const TOP_LEVEL_CHECKS = {
283
+ status: (doc, diagnostics) => checkEnumField(doc, "status", ENUM_VALUES.status, "status", diagnostics),
284
+ role: (doc, diagnostics) => checkEnumField(doc, "role", ENUM_VALUES.role, "role", diagnostics),
285
+ task_id: (doc, diagnostics) => checkNonEmptyStringField(doc, "task_id", "task_id", diagnostics),
286
+ summary: (doc, diagnostics) => checkArrayField(doc, "summary", diagnostics),
287
+ findings: checkFindings,
288
+ acceptance_recommendation: (doc, diagnostics) => checkEnumField(doc, "acceptance_recommendation", ENUM_VALUES.acceptance_recommendation, "acceptance_recommendation", diagnostics),
289
+ missing_tests: (doc, diagnostics) => checkArrayField(doc, "missing_tests", diagnostics),
290
+ residual_risks: (doc, diagnostics) => checkArrayField(doc, "residual_risks", diagnostics),
291
+ reproduction: checkReproduction,
292
+ method_applied: (doc, diagnostics) => checkEnumField(doc, "method_applied", ENUM_VALUES.method_applied, "method_applied", diagnostics),
293
+ withdrawn: checkWithdrawn,
294
+ };
295
+ /**
296
+ * The declared kind of every field of every contract constant, keyed by
297
+ * bare field name the way {@link ENUM_VALUES} is: a name is unique across
298
+ * the whole contract block except for `description`, which `findings[]`
299
+ * and `withdrawn[]` share with the same kind. The `satisfies
300
+ * Record<SchemaFieldName, FieldKind>` clause means a name added to any of
301
+ * the four constants without a kind here is a TypeScript compile error,
302
+ * the same way it is already an error to add one without a checker in the
303
+ * dispatch tables above. Were a future contract to reuse one name at two
304
+ * levels with two different kinds, this flat map could hold only one of
305
+ * them: the generator asserts each field's real diagnostics against its
306
+ * declared kind, so that shows up as a failing generated case rather than
307
+ * as an unchecked field.
308
+ */
309
+ export const FIELD_KINDS = {
310
+ status: "enum",
311
+ role: "enum",
312
+ task_id: "non-empty-string",
313
+ summary: "array",
314
+ findings: "mapping-list",
315
+ acceptance_recommendation: "enum",
316
+ missing_tests: "array",
317
+ residual_risks: "array",
318
+ reproduction: "mapping",
319
+ method_applied: "enum",
320
+ withdrawn: "mapping-list",
321
+ severity: "enum",
322
+ category: "enum",
323
+ description: "string",
324
+ suggested_fix: "string",
325
+ recurrence: "enum",
326
+ introduced_by_delta: "enum",
327
+ method: "scalar",
328
+ sample_size: "scalar",
329
+ result: "scalar",
330
+ matches_implementer_claim: "enum",
331
+ reason: "string",
332
+ };
333
+ /**
334
+ * The `expected` text a diagnostic carries, per kind. `enum` is absent on
335
+ * purpose: its text is the enum's own spellings, read from
336
+ * {@link ENUM_VALUES}.
337
+ */
338
+ const KIND_EXPECTED = {
339
+ string: "string",
340
+ "non-empty-string": "non-empty string",
341
+ scalar: "string or number",
342
+ array: "array",
343
+ "mapping-list": "array",
344
+ mapping: "mapping",
345
+ };
346
+ /**
347
+ * Fields whose diagnostic wording differs from their kind's default.
348
+ * `withdrawn`'s own text spells out that an empty list is fine, since a
349
+ * reviewer with nothing withdrawn must still emit the key.
350
+ */
351
+ const EXPECTED_OVERRIDES = {
352
+ withdrawn: "array (may be empty)",
353
+ };
354
+ /**
355
+ * The `expected` text this validator's diagnostics about `field` carry.
356
+ * The checkers above hold that text literally, at their own push sites;
357
+ * this is the schema's declaration of the same text, which the generated
358
+ * test cases assert the checkers actually produce, so a checker whose
359
+ * behaviour stops matching its declared kind fails a case instead of
360
+ * drifting quietly.
361
+ */
362
+ export function expectedTextFor(field) {
363
+ const override = EXPECTED_OVERRIDES[field];
364
+ if (override !== undefined)
365
+ return override;
366
+ const kind = FIELD_KINDS[field];
367
+ return kind === "enum" ? ENUM_VALUES[field].join(" | ") : KIND_EXPECTED[kind];
368
+ }
369
+ /** The `expected` text a diagnostic about one element of a `mapping-list` carries. */
370
+ export const MAPPING_LIST_ELEMENT_EXPECTED = KIND_EXPECTED.mapping;
371
+ /**
372
+ * A reviewer return is commonly wrapped in a single fenced code block.
373
+ * Strips one leading/trailing fence when present, whatever language tag
374
+ * it carries (```yaml, ```yml, a bare ``` , or any other tag -- a
375
+ * language-less fence previously fell through to the "literal YAML"
376
+ * branch below and produced a confusing YAML parse error instead of a
377
+ * clean structural diagnostic; fix-round, review finding L2); anything
378
+ * unfenced is treated as literal YAML. The fence is located anywhere in
379
+ * the input, not only at its very start: a return prefixed with prose
380
+ * ("Here is my report:\n```yaml ...") previously fell through to the
381
+ * "literal YAML" branch, since the fence pattern was anchored to the
382
+ * start of the string, and produced a raw parse error instead of a
383
+ * structural diagnostic (fix-round, review finding L1). Prose found
384
+ * before the opening fence or after the closing fence is tolerated, but
385
+ * each is named as its own warning rather than silently dropped.
386
+ *
387
+ * The closing fence must start at column 0: the pattern anchors it with
388
+ * `^` under the `m` flag, so a triple-backtick sequence inside a value
389
+ * (a reviewer quoting a fenced snippet in a `description` block scalar,
390
+ * which YAML necessarily indents) can no longer close the block early
391
+ * and hand the parser a truncated document, which surfaced as
392
+ * diagnostics about fields the return actually carried (fix-round,
393
+ * review finding L3).
394
+ */
395
+ export function extractYamlSource(raw) {
396
+ const warnings = [];
397
+ // No BOM handling: the yaml parser accepts a leading U+FEFF and the
398
+ // fenced path trims it away with the surrounding prose.
399
+ const withoutBom = raw;
400
+ const fenceMatch = withoutBom.match(/```[A-Za-z]*\r?\n([\s\S]*?)\r?\n?^```/m);
401
+ if (fenceMatch) {
402
+ const start = fenceMatch.index ?? 0;
403
+ const before = withoutBom.slice(0, start);
404
+ if (before.trim().length > 0) {
405
+ warnings.push("prose found before the opening ```yaml fence; only the fenced block was validated");
406
+ }
407
+ const inner = fenceMatch[1];
408
+ const after = withoutBom.slice(start + fenceMatch[0].length);
409
+ if (after.trim().length > 0) {
410
+ warnings.push("prose found after the closing ```yaml fence; only the fenced block was validated");
411
+ }
412
+ return { yamlText: inner, warnings };
413
+ }
414
+ return { yamlText: withoutBom, warnings };
415
+ }
416
+ /**
417
+ * Validates a reviewer return against the reviewer output contract's
418
+ * structure. Reports structural validity ONLY: it never judges semantic
419
+ * adequacy, never waives a finding, and passing it is never orchestrator
420
+ * acceptance.
421
+ */
422
+ export function validateReviewReport(raw) {
423
+ const diagnostics = [];
424
+ const { yamlText, warnings } = extractYamlSource(raw);
425
+ let parsed;
426
+ try {
427
+ parsed = parseYaml(yamlText);
428
+ }
429
+ catch (error) {
430
+ diagnostics.push({
431
+ path: "<root>",
432
+ expected: "valid YAML",
433
+ got: error instanceof Error ? error.message : String(error),
434
+ });
435
+ return { valid: false, diagnostics, warnings };
436
+ }
437
+ if (!isPlainRecord(parsed)) {
438
+ diagnostics.push({
439
+ path: "<root>",
440
+ expected: "a YAML mapping (object)",
441
+ got: describeValue(parsed),
442
+ });
443
+ return { valid: false, diagnostics, warnings };
444
+ }
445
+ for (const field of TOP_LEVEL_FIELDS) {
446
+ TOP_LEVEL_CHECKS[field](parsed, diagnostics);
447
+ }
448
+ return { valid: diagnostics.length === 0, diagnostics, warnings };
449
+ }
package/dist/uninstall.js CHANGED
@@ -74,15 +74,18 @@ const PRUNE_CANDIDATES = [
74
74
  join(".ai", "workflow"),
75
75
  join(".ai", "runs"),
76
76
  ".ai",
77
+ join(".claude", "skills", "orchestrator-workflow", "references"),
77
78
  join(".claude", "skills", "orchestrator-workflow"),
78
79
  join(".claude", "skills"),
79
80
  join(".claude", "agents"),
80
81
  ".claude",
82
+ join(".agents", "skills", "orchestrator-workflow", "references"),
81
83
  join(".agents", "skills", "orchestrator-workflow"),
82
84
  join(".agents", "skills"),
83
85
  ".agents",
84
86
  join(".codex", "agents"),
85
87
  ".codex",
88
+ join(".opencode", "skills", "orchestrator-workflow", "references"),
86
89
  join(".opencode", "skills", "orchestrator-workflow"),
87
90
  join(".opencode", "skills"),
88
91
  join(".opencode", "agents"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrator-workflow",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Installer for an orchestrator-led agent workflow: .ai/ run state, an AGENTS.md policy section, and per-harness subagent definitions for Claude Code, OpenAI Codex, and opencode",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -47,7 +47,8 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "commander": "^12.0.0",
50
- "inquirer": "^9.2.0"
50
+ "inquirer": "^9.2.0",
51
+ "yaml": "^2.9.1"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@iarna/toml": "^2.2.5",