agentweaver 0.1.17 → 0.1.18

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.
Files changed (47) hide show
  1. package/README.md +104 -23
  2. package/dist/artifacts.js +41 -0
  3. package/dist/index.js +252 -27
  4. package/dist/interactive/controller.js +249 -13
  5. package/dist/interactive/ink/index.js +2 -2
  6. package/dist/interactive/state.js +1 -0
  7. package/dist/interactive/web/index.js +179 -0
  8. package/dist/interactive/web/protocol.js +154 -0
  9. package/dist/interactive/web/server.js +575 -0
  10. package/dist/interactive/web/static/app.js +709 -0
  11. package/dist/interactive/web/static/index.html +77 -0
  12. package/dist/interactive/web/static/styles.css +2 -0
  13. package/dist/interactive/web/static/styles.input.css +469 -0
  14. package/dist/pipeline/flow-catalog.js +4 -0
  15. package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
  16. package/dist/pipeline/flow-specs/auto-common.json +3 -1
  17. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +2 -0
  18. package/dist/pipeline/flow-specs/design-review.json +2 -0
  19. package/dist/pipeline/flow-specs/implement.json +3 -1
  20. package/dist/pipeline/flow-specs/plan.json +4 -0
  21. package/dist/pipeline/flow-specs/playbook-init.json +199 -0
  22. package/dist/pipeline/flow-specs/review/review-fix.json +3 -1
  23. package/dist/pipeline/flow-specs/review/review-loop.json +4 -0
  24. package/dist/pipeline/flow-specs/review/review.json +2 -0
  25. package/dist/pipeline/node-registry.js +45 -0
  26. package/dist/pipeline/nodes/flow-run-node.js +13 -1
  27. package/dist/pipeline/nodes/playbook-ensure-node.js +115 -0
  28. package/dist/pipeline/nodes/playbook-inventory-node.js +51 -0
  29. package/dist/pipeline/nodes/playbook-questions-form-node.js +166 -0
  30. package/dist/pipeline/nodes/playbook-write-node.js +243 -0
  31. package/dist/pipeline/nodes/project-guidance-node.js +69 -0
  32. package/dist/pipeline/prompt-registry.js +4 -1
  33. package/dist/pipeline/prompt-runtime.js +6 -2
  34. package/dist/pipeline/spec-types.js +19 -0
  35. package/dist/pipeline/value-resolver.js +39 -1
  36. package/dist/playbook/practice-candidates.js +12 -0
  37. package/dist/playbook/repo-inventory.js +208 -0
  38. package/dist/prompts.js +31 -0
  39. package/dist/runtime/playbook.js +485 -0
  40. package/dist/runtime/project-guidance.js +339 -0
  41. package/dist/structured-artifact-schema-registry.js +8 -0
  42. package/dist/structured-artifact-schemas.json +235 -0
  43. package/dist/structured-artifacts.js +7 -1
  44. package/docs/declarative-workflows.md +565 -0
  45. package/docs/features.md +77 -0
  46. package/docs/playbook.md +327 -0
  47. package/package.json +8 -3
@@ -0,0 +1,485 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { TaskRunnerError } from "../errors.js";
5
+ const require = createRequire(import.meta.url);
6
+ export const PLAYBOOK_DIR = ".agentweaver/playbook";
7
+ export const PLAYBOOK_MANIFEST = "manifest.yaml";
8
+ export const SUPPORTED_PLAYBOOK_VERSION = 1;
9
+ export const SUPPORTED_PLAYBOOK_PHASES = ["plan", "design_review", "implement", "review", "repair"];
10
+ export const SUPPORTED_PLAYBOOK_SEVERITIES = ["must", "should", "info"];
11
+ export function loadProjectPlaybook(projectRoot) {
12
+ const root = path.resolve(projectRoot);
13
+ const playbookRoot = path.join(root, PLAYBOOK_DIR);
14
+ const manifestPath = path.join(playbookRoot, PLAYBOOK_MANIFEST);
15
+ if (!existsSync(manifestPath)) {
16
+ throw new TaskRunnerError(`Playbook manifest is missing: ${manifestPath}. Add ${PLAYBOOK_DIR}/${PLAYBOOK_MANIFEST}.`);
17
+ }
18
+ const manifest = parsePlaybookManifest(readFile(manifestPath), manifestPath);
19
+ const projectPath = resolvePlaybookFile(playbookRoot, "project.md", `${PLAYBOOK_MANIFEST}: project.md`);
20
+ const practiceFiles = resolveContentFiles(playbookRoot, manifest.practices, "practices");
21
+ const exampleFiles = resolveContentFiles(playbookRoot, manifest.examples, "examples");
22
+ const templateFiles = resolveContentFiles(playbookRoot, manifest.templates, "templates");
23
+ const alwaysIncludeFiles = manifest.always_include.map((entry, index) => resolvePlaybookFile(playbookRoot, entry, `${PLAYBOOK_MANIFEST}: always_include[${index}]`));
24
+ const practices = practiceFiles.map((filePath) => parsePlaybookMarkdownFile(filePath, playbookRoot, "practice"));
25
+ const examples = exampleFiles.map((filePath) => parsePlaybookMarkdownFile(filePath, playbookRoot, "example"));
26
+ validateUniqueIds([...practices, ...examples]);
27
+ validateRelationships([...practices, ...examples]);
28
+ return {
29
+ root,
30
+ playbookRoot,
31
+ manifestPath,
32
+ projectPath,
33
+ manifest,
34
+ projectMarkdown: readFile(projectPath),
35
+ practices,
36
+ examples,
37
+ templates: templateFiles.map((filePath) => toPlaybookRelative(playbookRoot, filePath)),
38
+ alwaysInclude: alwaysIncludeFiles.map((filePath) => toPlaybookRelative(playbookRoot, filePath)),
39
+ };
40
+ }
41
+ export function parsePlaybookManifest(source, filePath = PLAYBOOK_MANIFEST) {
42
+ const parsed = parseYaml(source, filePath);
43
+ const manifest = requireRecord(parsed, filePath, "manifest");
44
+ const version = manifest.version;
45
+ if (version !== SUPPORTED_PLAYBOOK_VERSION) {
46
+ throw validationError(filePath, "version", `Unsupported playbook version ${String(version)}. Supported version: ${SUPPORTED_PLAYBOOK_VERSION}.`);
47
+ }
48
+ const project = requireRecord(manifest.project, filePath, "project");
49
+ const normalized = {
50
+ version: SUPPORTED_PLAYBOOK_VERSION,
51
+ project: {
52
+ name: requireString(project.name, filePath, "project.name"),
53
+ ...optionalStringArrayField(project, filePath, "project", "stack"),
54
+ ...optionalStringArrayField(project, filePath, "project", "languages"),
55
+ ...optionalStringArrayField(project, filePath, "project", "frameworks"),
56
+ },
57
+ context_budgets: parseContextBudgets(manifest.context_budgets, filePath),
58
+ practices: parseContentPaths(manifest.practices, filePath, "practices"),
59
+ examples: parseContentPaths(manifest.examples, filePath, "examples"),
60
+ templates: parseContentPaths(manifest.templates, filePath, "templates"),
61
+ always_include: optionalStringArray(manifest.always_include, filePath, "always_include"),
62
+ selection: parseSelection(manifest.selection, filePath),
63
+ };
64
+ return normalized;
65
+ }
66
+ export function parseMarkdownFrontmatter(source, filePath) {
67
+ if (!source.startsWith("---\n") && source.trim() !== "---") {
68
+ throw validationError(filePath, "frontmatter", "Markdown file must start with YAML frontmatter delimited by ---. ");
69
+ }
70
+ const closingIndex = source.indexOf("\n---", 4);
71
+ if (closingIndex === -1) {
72
+ throw validationError(filePath, "frontmatter", "Markdown frontmatter is missing the closing --- delimiter.");
73
+ }
74
+ const frontmatterSource = source.slice(4, closingIndex);
75
+ const afterDelimiter = source.slice(closingIndex + 4);
76
+ const body = afterDelimiter.startsWith("\n") ? afterDelimiter.slice(1) : afterDelimiter;
77
+ return {
78
+ frontmatter: parseYaml(frontmatterSource, filePath),
79
+ body,
80
+ };
81
+ }
82
+ export function validateProjectPlaybook(playbook) {
83
+ validateUniqueIds([...playbook.practices, ...playbook.examples]);
84
+ validateRelationships([...playbook.practices, ...playbook.examples]);
85
+ return playbook;
86
+ }
87
+ function parsePlaybookMarkdownFile(filePath, playbookRoot, kind) {
88
+ const { frontmatter, body } = parseMarkdownFrontmatter(readFile(filePath), filePath);
89
+ const metadata = parseMarkdownMetadata(frontmatter, filePath);
90
+ return {
91
+ kind,
92
+ id: metadata.id,
93
+ title: metadata.title,
94
+ path: toPlaybookRelative(playbookRoot, filePath),
95
+ absolutePath: filePath,
96
+ body,
97
+ metadata,
98
+ };
99
+ }
100
+ function parseMarkdownMetadata(value, filePath) {
101
+ const metadata = requireRecord(value, filePath, "frontmatter");
102
+ const parsed = {
103
+ id: requireString(metadata.id, filePath, "frontmatter.id"),
104
+ title: requireString(metadata.title, filePath, "frontmatter.title"),
105
+ phases: parsePhases(metadata.phases, filePath, "frontmatter.phases"),
106
+ related_practices: optionalStringArray(metadata.related_practices, filePath, "frontmatter.related_practices"),
107
+ related_examples: optionalStringArray(metadata.related_examples, filePath, "frontmatter.related_examples"),
108
+ };
109
+ const appliesTo = parseAppliesTo(metadata.applies_to, filePath);
110
+ const priority = parseOptionalNonNegativeInteger(metadata.priority, filePath, "frontmatter.priority");
111
+ const severity = parseOptionalSeverity(metadata.severity, filePath, "frontmatter.severity");
112
+ if (appliesTo !== undefined) {
113
+ parsed.applies_to = appliesTo;
114
+ }
115
+ if (priority !== undefined) {
116
+ parsed.priority = priority;
117
+ }
118
+ if (severity !== undefined) {
119
+ parsed.severity = severity;
120
+ }
121
+ return parsed;
122
+ }
123
+ function parseContextBudgets(value, filePath) {
124
+ const result = {};
125
+ if (value === undefined) {
126
+ return result;
127
+ }
128
+ const budgets = requireRecord(value, filePath, "context_budgets");
129
+ for (const [phase, budget] of Object.entries(budgets)) {
130
+ if (!isSupportedPhase(phase)) {
131
+ throw validationError(filePath, `context_budgets.${phase}`, `Unsupported context budget phase. Supported phases: ${SUPPORTED_PLAYBOOK_PHASES.join(", ")}.`);
132
+ }
133
+ result[phase] = requireNonNegativeInteger(budget, filePath, `context_budgets.${phase}`);
134
+ }
135
+ return result;
136
+ }
137
+ function parseContentPaths(value, filePath, fieldPath) {
138
+ const paths = requireRecord(value, filePath, fieldPath);
139
+ const parsed = {
140
+ paths: optionalStringArray(paths.paths, filePath, `${fieldPath}.paths`),
141
+ globs: optionalStringArray(paths.globs, filePath, `${fieldPath}.globs`),
142
+ };
143
+ if (parsed.paths.length === 0 && parsed.globs.length === 0) {
144
+ throw validationError(filePath, fieldPath, "Expected at least one path or glob declaration.");
145
+ }
146
+ return parsed;
147
+ }
148
+ function parseSelection(value, filePath) {
149
+ if (value === undefined) {
150
+ return {};
151
+ }
152
+ const selection = requireRecord(value, filePath, "selection");
153
+ const result = {};
154
+ if (selection.include_examples !== undefined) {
155
+ if (typeof selection.include_examples !== "boolean") {
156
+ throw validationError(filePath, "selection.include_examples", "Expected a boolean value.");
157
+ }
158
+ result.include_examples = selection.include_examples;
159
+ }
160
+ if (selection.max_examples !== undefined) {
161
+ result.max_examples = requireNonNegativeInteger(selection.max_examples, filePath, "selection.max_examples");
162
+ }
163
+ return result;
164
+ }
165
+ function parseAppliesTo(value, filePath) {
166
+ if (value === undefined) {
167
+ return undefined;
168
+ }
169
+ const appliesTo = requireRecord(value, filePath, "frontmatter.applies_to");
170
+ return {
171
+ ...optionalStringArrayField(appliesTo, filePath, "frontmatter.applies_to", "languages"),
172
+ ...optionalStringArrayField(appliesTo, filePath, "frontmatter.applies_to", "frameworks"),
173
+ ...optionalStringArrayField(appliesTo, filePath, "frontmatter.applies_to", "globs"),
174
+ ...optionalStringArrayField(appliesTo, filePath, "frontmatter.applies_to", "keywords"),
175
+ };
176
+ }
177
+ function parsePhases(value, filePath, fieldPath) {
178
+ const phases = optionalStringArray(value, filePath, fieldPath);
179
+ for (const phase of phases) {
180
+ if (!isSupportedPhase(phase)) {
181
+ throw validationError(filePath, fieldPath, `Unsupported phase "${phase}". Supported phases: ${SUPPORTED_PLAYBOOK_PHASES.join(", ")}.`);
182
+ }
183
+ }
184
+ return phases;
185
+ }
186
+ function parseOptionalSeverity(value, filePath, fieldPath) {
187
+ if (value === undefined) {
188
+ return undefined;
189
+ }
190
+ if (typeof value !== "string" || !isSupportedSeverity(value)) {
191
+ throw validationError(filePath, fieldPath, `Unsupported severity "${String(value)}". Supported severities: ${SUPPORTED_PLAYBOOK_SEVERITIES.join(", ")}.`);
192
+ }
193
+ return value;
194
+ }
195
+ function parseOptionalNonNegativeInteger(value, filePath, fieldPath) {
196
+ if (value === undefined) {
197
+ return undefined;
198
+ }
199
+ return requireNonNegativeInteger(value, filePath, fieldPath);
200
+ }
201
+ function resolveContentFiles(playbookRoot, contentPaths, fieldPath) {
202
+ const files = new Set();
203
+ contentPaths.paths.forEach((entry, index) => {
204
+ files.add(resolvePlaybookFile(playbookRoot, entry, `${PLAYBOOK_MANIFEST}: ${fieldPath}.paths[${index}]`));
205
+ });
206
+ contentPaths.globs.forEach((entry, index) => {
207
+ const matches = resolvePlaybookGlob(playbookRoot, entry, `${PLAYBOOK_MANIFEST}: ${fieldPath}.globs[${index}]`);
208
+ for (const match of matches) {
209
+ files.add(match);
210
+ }
211
+ });
212
+ return [...files].sort();
213
+ }
214
+ function resolvePlaybookFile(playbookRoot, relativePath, sourceField) {
215
+ const resolved = resolveInsidePlaybook(playbookRoot, relativePath, sourceField);
216
+ if (!existsSync(resolved)) {
217
+ throw new TaskRunnerError(`Missing playbook file referenced by ${sourceField}: ${resolved}. Create the file or update the manifest path.`);
218
+ }
219
+ if (!statSync(resolved).isFile()) {
220
+ throw new TaskRunnerError(`Playbook path referenced by ${sourceField} is not a file: ${resolved}.`);
221
+ }
222
+ return resolved;
223
+ }
224
+ function resolvePlaybookGlob(playbookRoot, pattern, sourceField) {
225
+ resolveInsidePlaybook(playbookRoot, pattern.replace(/\*/g, "placeholder"), sourceField);
226
+ const matches = findGlobMatches(playbookRoot, pattern);
227
+ if (matches.length === 0) {
228
+ throw new TaskRunnerError(`Playbook glob referenced by ${sourceField} matched no files: ${pattern}. Add matching files or update manifest.yaml.`);
229
+ }
230
+ return matches;
231
+ }
232
+ function resolveInsidePlaybook(playbookRoot, relativePath, sourceField) {
233
+ if (path.isAbsolute(relativePath)) {
234
+ throw new TaskRunnerError(`Playbook path referenced by ${sourceField} must be relative to ${playbookRoot}: ${relativePath}.`);
235
+ }
236
+ const resolved = path.resolve(playbookRoot, relativePath);
237
+ const relative = path.relative(playbookRoot, resolved);
238
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
239
+ throw new TaskRunnerError(`Playbook path traversal is not allowed in ${sourceField}: ${relativePath}. Keep paths inside ${playbookRoot}.`);
240
+ }
241
+ return resolved;
242
+ }
243
+ function findGlobMatches(playbookRoot, pattern) {
244
+ const regex = globToRegex(pattern);
245
+ const results = [];
246
+ for (const filePath of walkFiles(playbookRoot)) {
247
+ const relative = toPlaybookRelative(playbookRoot, filePath);
248
+ if (regex.test(relative)) {
249
+ results.push(filePath);
250
+ }
251
+ }
252
+ return results.sort();
253
+ }
254
+ function globToRegex(pattern) {
255
+ let source = "^";
256
+ for (let index = 0; index < pattern.length; index += 1) {
257
+ const character = pattern[index];
258
+ const next = pattern[index + 1];
259
+ if (character === "*" && next === "*") {
260
+ source += ".*";
261
+ index += 1;
262
+ }
263
+ else if (character === "*") {
264
+ source += "[^/]*";
265
+ }
266
+ else if (character === "?") {
267
+ source += "[^/]";
268
+ }
269
+ else {
270
+ source += escapeRegex(character ?? "");
271
+ }
272
+ }
273
+ return new RegExp(`${source}$`);
274
+ }
275
+ function walkFiles(directory) {
276
+ if (!existsSync(directory)) {
277
+ return [];
278
+ }
279
+ const entries = readdirSync(directory, { withFileTypes: true });
280
+ const files = [];
281
+ for (const entry of entries) {
282
+ const entryPath = path.join(directory, entry.name);
283
+ if (entry.isDirectory()) {
284
+ files.push(...walkFiles(entryPath));
285
+ }
286
+ else if (entry.isFile()) {
287
+ files.push(entryPath);
288
+ }
289
+ }
290
+ return files;
291
+ }
292
+ function validateUniqueIds(entries) {
293
+ const seen = new Map();
294
+ for (const entry of entries) {
295
+ const previous = seen.get(entry.id);
296
+ if (previous) {
297
+ throw new TaskRunnerError(`Duplicate playbook id "${entry.id}" in ${entry.absolutePath}; first declared in ${previous.absolutePath}. Use unique ids across practices and examples.`);
298
+ }
299
+ seen.set(entry.id, entry);
300
+ }
301
+ }
302
+ function validateRelationships(entries) {
303
+ const ids = new Set(entries.map((entry) => entry.id));
304
+ for (const entry of entries) {
305
+ for (const relatedId of [...entry.metadata.related_practices, ...entry.metadata.related_examples]) {
306
+ if (!ids.has(relatedId)) {
307
+ throw new TaskRunnerError(`Unknown playbook relationship id "${relatedId}" referenced by ${entry.absolutePath}. Add the related file or update the frontmatter reference.`);
308
+ }
309
+ }
310
+ }
311
+ }
312
+ function parseYaml(source, filePath) {
313
+ try {
314
+ const yaml = require("yaml");
315
+ if (typeof yaml.parseDocument === "function") {
316
+ const document = yaml.parseDocument(source);
317
+ if (document.errors.length > 0) {
318
+ throw document.errors[0];
319
+ }
320
+ return document.toJSON();
321
+ }
322
+ }
323
+ catch (error) {
324
+ if (isYamlSyntaxError(error)) {
325
+ throw yamlError(filePath, error);
326
+ }
327
+ }
328
+ try {
329
+ return parseSimpleYaml(source);
330
+ }
331
+ catch (error) {
332
+ throw yamlError(filePath, error);
333
+ }
334
+ }
335
+ function parseSimpleYaml(source) {
336
+ const lines = source
337
+ .replace(/\r\n/g, "\n")
338
+ .split("\n")
339
+ .map((raw) => ({ raw, indent: raw.search(/\S|$/), text: raw.trim() }))
340
+ .filter((line) => line.text.length > 0 && !line.text.startsWith("#"));
341
+ let index = 0;
342
+ function parseBlock(indent) {
343
+ if (index >= lines.length) {
344
+ return {};
345
+ }
346
+ if (lines[index]?.indent === indent && lines[index]?.text.startsWith("- ")) {
347
+ return parseArray(indent);
348
+ }
349
+ return parseObject(indent);
350
+ }
351
+ function parseObject(indent) {
352
+ const result = {};
353
+ while (index < lines.length) {
354
+ const line = lines[index];
355
+ if (!line || line.indent < indent || line.text.startsWith("- ")) {
356
+ break;
357
+ }
358
+ if (line.indent > indent) {
359
+ throw new Error(`Unexpected indentation near "${line.text}".`);
360
+ }
361
+ const separator = line.text.indexOf(":");
362
+ if (separator === -1) {
363
+ throw new Error(`Expected key/value pair near "${line.text}".`);
364
+ }
365
+ const key = line.text.slice(0, separator).trim();
366
+ const rest = line.text.slice(separator + 1).trim();
367
+ index += 1;
368
+ result[key] = rest.length > 0 ? parseScalar(rest) : parseBlock(indent + 2);
369
+ }
370
+ return result;
371
+ }
372
+ function parseArray(indent) {
373
+ const result = [];
374
+ while (index < lines.length) {
375
+ const line = lines[index];
376
+ if (!line || line.indent !== indent || !line.text.startsWith("- ")) {
377
+ break;
378
+ }
379
+ const item = line.text.slice(2).trim();
380
+ index += 1;
381
+ result.push(item.length > 0 ? parseScalar(item) : parseBlock(indent + 2));
382
+ }
383
+ return result;
384
+ }
385
+ const parsed = parseBlock(0);
386
+ if (index < lines.length) {
387
+ throw new Error(`Could not parse YAML near "${lines[index]?.text ?? ""}".`);
388
+ }
389
+ return parsed;
390
+ }
391
+ function parseScalar(value) {
392
+ if (value === "true") {
393
+ return true;
394
+ }
395
+ if (value === "false") {
396
+ return false;
397
+ }
398
+ if (value === "null" || value === "~") {
399
+ return null;
400
+ }
401
+ if (/^-?\d+$/.test(value)) {
402
+ return Number(value);
403
+ }
404
+ if (value.startsWith("[") !== value.endsWith("]")) {
405
+ throw new Error(`Malformed inline array near "${value}".`);
406
+ }
407
+ if (value.startsWith("[") && value.endsWith("]")) {
408
+ const inner = value.slice(1, -1).trim();
409
+ return inner.length === 0 ? [] : inner.split(",").map((item) => parseScalar(item.trim()));
410
+ }
411
+ if ((value.startsWith('"') && value.endsWith('"')) ||
412
+ (value.startsWith("'") && value.endsWith("'"))) {
413
+ return value.slice(1, -1);
414
+ }
415
+ if (value.includes(": ")) {
416
+ throw new Error(`Unsupported inline mapping near "${value}".`);
417
+ }
418
+ return value;
419
+ }
420
+ function requireRecord(value, filePath, fieldPath) {
421
+ if (!isRecord(value)) {
422
+ throw validationError(filePath, fieldPath, "Expected an object.");
423
+ }
424
+ return value;
425
+ }
426
+ function requireString(value, filePath, fieldPath) {
427
+ if (typeof value !== "string" || value.trim().length === 0) {
428
+ throw validationError(filePath, fieldPath, "Expected a non-empty string.");
429
+ }
430
+ return value;
431
+ }
432
+ function optionalStringArray(value, filePath, fieldPath) {
433
+ if (value === undefined) {
434
+ return [];
435
+ }
436
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
437
+ throw validationError(filePath, fieldPath, "Expected an array of non-empty strings.");
438
+ }
439
+ return value;
440
+ }
441
+ function optionalStringArrayField(record, filePath, parentFieldPath, fieldName) {
442
+ if (record[fieldName] === undefined) {
443
+ return {};
444
+ }
445
+ return {
446
+ [fieldName]: optionalStringArray(record[fieldName], filePath, `${parentFieldPath}.${fieldName}`),
447
+ };
448
+ }
449
+ function requireNonNegativeInteger(value, filePath, fieldPath) {
450
+ if (!Number.isInteger(value) || Number(value) < 0) {
451
+ throw validationError(filePath, fieldPath, "Expected a non-negative integer.");
452
+ }
453
+ return Number(value);
454
+ }
455
+ function validationError(filePath, fieldPath, details) {
456
+ return new TaskRunnerError(`Invalid playbook file ${filePath} at ${fieldPath}: ${details}`);
457
+ }
458
+ function yamlError(filePath, error) {
459
+ const message = error instanceof Error ? error.message : String(error);
460
+ return new TaskRunnerError(`Invalid YAML in playbook file ${filePath}: ${message}`);
461
+ }
462
+ function isYamlSyntaxError(error) {
463
+ if (!(error instanceof Error)) {
464
+ return false;
465
+ }
466
+ return error.name.includes("YAML") || error.message.includes("YAML") || error.message.includes("Map keys");
467
+ }
468
+ function isRecord(value) {
469
+ return typeof value === "object" && value !== null && !Array.isArray(value);
470
+ }
471
+ function isSupportedPhase(value) {
472
+ return SUPPORTED_PLAYBOOK_PHASES.includes(value);
473
+ }
474
+ function isSupportedSeverity(value) {
475
+ return SUPPORTED_PLAYBOOK_SEVERITIES.includes(value);
476
+ }
477
+ function readFile(filePath) {
478
+ return readFileSync(filePath, "utf8");
479
+ }
480
+ function toPlaybookRelative(playbookRoot, filePath) {
481
+ return path.relative(playbookRoot, filePath).split(path.sep).join("/");
482
+ }
483
+ function escapeRegex(value) {
484
+ return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
485
+ }