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,339 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { TaskRunnerError } from "../errors.js";
4
+ import { loadProjectPlaybook, PLAYBOOK_DIR, PLAYBOOK_MANIFEST, } from "./playbook.js";
5
+ export const GUIDANCE_PHASES = ["plan", "design-review", "implement", "review", "repair/review-fix"];
6
+ export const DEFAULT_GUIDANCE_BUDGETS = {
7
+ plan: 1200,
8
+ "design-review": 1000,
9
+ implement: 1400,
10
+ review: 1000,
11
+ "repair/review-fix": 1000,
12
+ };
13
+ export const DEFAULT_INLINE_THRESHOLD = 300;
14
+ const LANGUAGE_HINTS = ["typescript", "javascript", "node", "go", "golang", "python", "react", "vue", "svelte"];
15
+ const FRAMEWORK_HINTS = ["express", "fastify", "nestjs", "react", "vite", "vitest", "node:test", "jest", "ink"];
16
+ export function normalizeGuidancePhase(value) {
17
+ const normalized = value.trim().toLowerCase().replace(/_/g, "-");
18
+ if (normalized === "design_review") {
19
+ return "design-review";
20
+ }
21
+ if (normalized === "repair" || normalized === "review-fix") {
22
+ return "repair/review-fix";
23
+ }
24
+ if (GUIDANCE_PHASES.includes(normalized)) {
25
+ return normalized;
26
+ }
27
+ throw new TaskRunnerError(`Unsupported project guidance phase '${value}'. Supported phases: ${GUIDANCE_PHASES.join(", ")}.`);
28
+ }
29
+ export function toPlaybookPhase(phase) {
30
+ switch (phase) {
31
+ case "design-review":
32
+ return "design_review";
33
+ case "repair/review-fix":
34
+ return "repair";
35
+ default:
36
+ return phase;
37
+ }
38
+ }
39
+ export function getDefaultGuidanceBudget(phase) {
40
+ return { limit: DEFAULT_GUIDANCE_BUDGETS[phase], inlineThreshold: DEFAULT_INLINE_THRESHOLD };
41
+ }
42
+ export function estimateApproxTokens(value) {
43
+ return Math.ceil(value.length / 4);
44
+ }
45
+ export function extractTaskSignals(taskContext) {
46
+ const values = [];
47
+ collectStrings(taskContext, values);
48
+ const joined = values.join("\n");
49
+ const keywords = Array.from(new Set(joined.toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) ?? []))
50
+ .filter((word) => !STOP_WORDS.has(word))
51
+ .sort();
52
+ const referencedPaths = Array.from(new Set(joined.match(/(?:^|\s)([./]?[A-Za-z0-9_.@-]+\/[A-Za-z0-9_./@-]+)/g)?.map((item) => item.trim()) ?? []))
53
+ .map((item) => item.replace(/^["'`]|["'`,.;:]$/g, ""))
54
+ .sort();
55
+ const languages = LANGUAGE_HINTS.filter((hint) => keywords.includes(hint));
56
+ const frameworks = FRAMEWORK_HINTS.filter((hint) => keywords.includes(hint));
57
+ const record = isRecord(taskContext) ? taskContext : {};
58
+ return {
59
+ title: firstString(record.title, record.summary, record.key) || "Untitled task",
60
+ source_type: firstString(record.source_type, record.sourceType) || "unknown",
61
+ keywords,
62
+ file_globs: referencedPaths,
63
+ referenced_paths: referencedPaths,
64
+ languages,
65
+ frameworks,
66
+ };
67
+ }
68
+ export function buildProjectGuidance(options) {
69
+ const phase = normalizeGuidancePhase(options.phase);
70
+ const playbookRoot = path.join(path.resolve(options.projectRoot), PLAYBOOK_DIR);
71
+ const manifestPath = path.join(playbookRoot, PLAYBOOK_MANIFEST);
72
+ const defaults = getDefaultGuidanceBudget(phase);
73
+ const budget = makeBudget(options.budgetLimit ?? defaults.limit, options.inlineThreshold ?? defaults.inlineThreshold);
74
+ const taskSignals = extractTaskSignals(options.taskContext);
75
+ if (!existsSync(manifestPath)) {
76
+ return {
77
+ summary: `No project playbook manifest was found for ${phase} guidance.`,
78
+ status: "missing_playbook",
79
+ phase,
80
+ source_playbook: { root_dir: playbookRoot, manifest_path: manifestPath, exists: false, valid: false },
81
+ task_signals: taskSignals,
82
+ selected_practices: [],
83
+ selected_examples: [],
84
+ always_include: [],
85
+ phase_sections: [],
86
+ budget,
87
+ skipped_items: [],
88
+ warnings: [{ code: "missing_playbook", message: "Project guidance was not generated because manifest.yaml is absent.", severity: "warning" }],
89
+ };
90
+ }
91
+ let playbook;
92
+ try {
93
+ playbook = loadProjectPlaybook(options.projectRoot);
94
+ }
95
+ catch (error) {
96
+ const message = error.message;
97
+ if ((options.invalidPlaybookPolicy ?? "fail_before_prompt") === "write_diagnostic_artifact") {
98
+ return {
99
+ summary: `The project playbook manifest is invalid for ${phase} guidance.`,
100
+ status: "invalid_playbook",
101
+ phase,
102
+ source_playbook: { root_dir: playbookRoot, manifest_path: manifestPath, exists: true, valid: false, error: message },
103
+ task_signals: taskSignals,
104
+ selected_practices: [],
105
+ selected_examples: [],
106
+ always_include: [],
107
+ phase_sections: [],
108
+ budget,
109
+ skipped_items: [],
110
+ warnings: [{ code: "invalid_playbook", message, severity: "error" }],
111
+ };
112
+ }
113
+ throw new TaskRunnerError(`Invalid project playbook ${manifestPath}: ${message}`);
114
+ }
115
+ const scored = [...playbook.practices, ...playbook.examples]
116
+ .map((entry) => scoreEntry(entry, playbook, phase, taskSignals))
117
+ .filter((entry) => entry !== null)
118
+ .sort(compareScoredEntries);
119
+ const skipped = [];
120
+ const selected = [];
121
+ for (const scoredEntry of scored) {
122
+ selected.push(materializeEntry(scoredEntry, playbook, budget, skipped));
123
+ }
124
+ const selectedPractices = selected.filter((entry) => entry.kind === "practice");
125
+ const selectedExamples = selected.filter((entry) => entry.kind === "example");
126
+ const status = selected.length > 0 || playbook.alwaysInclude.length > 0 ? "available" : "empty_selection";
127
+ return {
128
+ summary: status === "available"
129
+ ? `Selected compact ${phase} project guidance from manifest.yaml.`
130
+ : `No matching project guidance was selected for ${phase}.`,
131
+ status,
132
+ phase,
133
+ source_playbook: { root_dir: playbook.playbookRoot, manifest_path: playbook.manifestPath, exists: true, valid: true },
134
+ task_signals: taskSignals,
135
+ selected_practices: selectedPractices,
136
+ selected_examples: selectedExamples,
137
+ always_include: playbook.alwaysInclude,
138
+ phase_sections: selectedPractices.map((entry) => entry.title),
139
+ budget,
140
+ skipped_items: skipped,
141
+ warnings: [],
142
+ };
143
+ }
144
+ export function renderProjectGuidanceMarkdown(guidance, language = "en") {
145
+ if (language === "ru") {
146
+ return renderRussianMarkdown(guidance);
147
+ }
148
+ return renderEnglishMarkdown(guidance);
149
+ }
150
+ function scoreEntry(entry, playbook, phase, signals) {
151
+ const playbookPhase = toPlaybookPhase(phase);
152
+ const phases = entry.metadata.phases;
153
+ const always = playbook.alwaysInclude.includes(entry.path);
154
+ const reasons = [];
155
+ let score = 0;
156
+ if (always) {
157
+ score += 1000;
158
+ reasons.push("always_include");
159
+ }
160
+ if (phases.length === 0 || phases.includes(playbookPhase)) {
161
+ score += 100;
162
+ reasons.push("phase_match");
163
+ }
164
+ else if (!always) {
165
+ return null;
166
+ }
167
+ const appliesTo = entry.metadata.applies_to;
168
+ const text = `${entry.id} ${entry.title} ${entry.body}`.toLowerCase();
169
+ const keywordMatches = (appliesTo?.keywords ?? signals.keywords).filter((keyword) => signals.keywords.includes(keyword.toLowerCase()) || text.includes(keyword.toLowerCase()));
170
+ if (keywordMatches.length > 0) {
171
+ score += Math.min(keywordMatches.length, 10) * 8;
172
+ reasons.push("keyword_match");
173
+ }
174
+ if ((appliesTo?.globs ?? []).some((glob) => signals.referenced_paths.some((filePath) => globMatches(glob, filePath)))) {
175
+ score += 40;
176
+ reasons.push("glob_match");
177
+ }
178
+ if ((appliesTo?.languages ?? []).some((language) => signals.languages.includes(language.toLowerCase()))) {
179
+ score += 30;
180
+ reasons.push("language_match");
181
+ }
182
+ if ((appliesTo?.frameworks ?? []).some((framework) => signals.frameworks.includes(framework.toLowerCase()))) {
183
+ score += 30;
184
+ reasons.push("framework_match");
185
+ }
186
+ score += (entry.metadata.priority ?? 0) * 5;
187
+ if (entry.metadata.priority !== undefined) {
188
+ reasons.push("priority");
189
+ }
190
+ score += severityWeight(entry.metadata.severity);
191
+ if (entry.metadata.severity) {
192
+ reasons.push("severity");
193
+ }
194
+ if (score <= 0) {
195
+ return null;
196
+ }
197
+ return { entry, score, reasons: Array.from(new Set(reasons)), always };
198
+ }
199
+ function materializeEntry(scored, playbook, budget, skipped) {
200
+ const safePath = containedPlaybookPath(playbook, scored.entry.path);
201
+ const tokens = estimateApproxTokens(scored.entry.body);
202
+ const referenceReason = scored.entry.kind === "example" ? "example_reference_only" : "over_budget";
203
+ const item = {
204
+ id: scored.entry.id,
205
+ title: scored.entry.title,
206
+ kind: scored.entry.kind,
207
+ source_path: safePath,
208
+ selection_reasons: scored.reasons,
209
+ relevance_score: scored.score,
210
+ priority: scored.entry.metadata.priority ?? 0,
211
+ severity: scored.entry.metadata.severity ?? "info",
212
+ reference_only: true,
213
+ reference: { source_path: safePath, reason: referenceReason },
214
+ };
215
+ if (scored.entry.kind === "practice" && tokens <= budget.inline_threshold && tokens <= budget.remaining) {
216
+ item.inline_content = scored.entry.body.trim();
217
+ item.reference_only = false;
218
+ item.reference.reason = "inlined";
219
+ budget.used += tokens;
220
+ budget.remaining = Math.max(0, budget.limit - budget.used);
221
+ }
222
+ else if (tokens > budget.inline_threshold || tokens > budget.remaining) {
223
+ skipped.push({ id: scored.entry.id, kind: scored.entry.kind, source_path: safePath, reason: tokens > budget.inline_threshold ? "inline_threshold_exceeded" : "over_budget" });
224
+ }
225
+ return item;
226
+ }
227
+ function containedPlaybookPath(playbook, relativePath) {
228
+ if (path.isAbsolute(relativePath)) {
229
+ throw new TaskRunnerError(`Playbook reference must be relative: ${relativePath}`);
230
+ }
231
+ const resolved = path.resolve(playbook.playbookRoot, relativePath);
232
+ const relative = path.relative(playbook.playbookRoot, resolved);
233
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
234
+ throw new TaskRunnerError(`Playbook reference escapes ${PLAYBOOK_DIR}: ${relativePath}`);
235
+ }
236
+ return relative.split(path.sep).join("/");
237
+ }
238
+ function compareScoredEntries(left, right) {
239
+ return (Number(right.always) - Number(left.always) ||
240
+ right.score - left.score ||
241
+ (right.entry.metadata.priority ?? 0) - (left.entry.metadata.priority ?? 0) ||
242
+ severityWeight(right.entry.metadata.severity) - severityWeight(left.entry.metadata.severity) ||
243
+ left.entry.id.localeCompare(right.entry.id));
244
+ }
245
+ function severityWeight(severity) {
246
+ if (severity === "must") {
247
+ return 30;
248
+ }
249
+ if (severity === "should") {
250
+ return 10;
251
+ }
252
+ return 0;
253
+ }
254
+ function makeBudget(limit, inlineThreshold) {
255
+ return {
256
+ limit: Math.max(0, Math.floor(limit)),
257
+ used: 0,
258
+ remaining: Math.max(0, Math.floor(limit)),
259
+ inline_threshold: Math.max(0, Math.floor(inlineThreshold)),
260
+ unit: "approx_tokens",
261
+ estimator: "chars_div_4",
262
+ };
263
+ }
264
+ function renderRussianMarkdown(guidance) {
265
+ const lines = [`# Проектные рекомендации: ${guidance.phase}`, "", `Статус: ${guidance.status}`, "", "## Обязательные правила"];
266
+ if (guidance.status === "missing_playbook") {
267
+ lines.push("- Проектный playbook manifest.yaml не найден; дополнительных проектных рекомендаций нет.");
268
+ }
269
+ else if (guidance.selected_practices.length === 0) {
270
+ lines.push("- Для этой фазы не выбраны дополнительные правила.");
271
+ }
272
+ else {
273
+ for (const item of guidance.selected_practices) {
274
+ lines.push(`- ${item.title} (${item.source_path}): ${item.selection_reasons.join(", ")}`);
275
+ if (item.inline_content) {
276
+ lines.push(` ${item.inline_content.replace(/\n/g, " ")}`);
277
+ }
278
+ }
279
+ }
280
+ lines.push("", "## Релевантные примеры и ссылки");
281
+ const refs = [...guidance.selected_examples, ...guidance.selected_practices.filter((item) => item.reference_only)];
282
+ lines.push(...(refs.length === 0 ? ["- Нет релевантных ссылок."] : refs.map((item) => `- ${item.title}: ${item.source_path} (${item.reference.reason})`)));
283
+ lines.push("", "Открывайте полные примеры только когда они напрямую релевантны текущему изменению.");
284
+ lines.push("", "## Бюджет", `Использовано ${guidance.budget.used} из ${guidance.budget.limit} ${guidance.budget.unit}; осталось ${guidance.budget.remaining}.`);
285
+ return `${lines.join("\n")}\n`;
286
+ }
287
+ function renderEnglishMarkdown(guidance) {
288
+ const lines = [`# Project Guidance: ${guidance.phase}`, "", `Status: ${guidance.status}`, "", "## Must-Follow Rules"];
289
+ if (guidance.status === "missing_playbook") {
290
+ lines.push("- Project playbook manifest.yaml was not found; no additional project guidance is available.");
291
+ }
292
+ else if (guidance.selected_practices.length === 0) {
293
+ lines.push("- No additional rules were selected for this phase.");
294
+ }
295
+ else {
296
+ for (const item of guidance.selected_practices) {
297
+ lines.push(`- ${item.title} (${item.source_path}): ${item.selection_reasons.join(", ")}`);
298
+ if (item.inline_content) {
299
+ lines.push(` ${item.inline_content.replace(/\n/g, " ")}`);
300
+ }
301
+ }
302
+ }
303
+ lines.push("", "## Relevant Examples And References");
304
+ const refs = [...guidance.selected_examples, ...guidance.selected_practices.filter((item) => item.reference_only)];
305
+ lines.push(...(refs.length === 0 ? ["- No relevant references."] : refs.map((item) => `- ${item.title}: ${item.source_path} (${item.reference.reason})`)));
306
+ lines.push("", "Open full examples only when they are directly relevant to the current change.");
307
+ lines.push("", "## Budget", `Used ${guidance.budget.used} of ${guidance.budget.limit} ${guidance.budget.unit}; remaining ${guidance.budget.remaining}.`);
308
+ return `${lines.join("\n")}\n`;
309
+ }
310
+ function collectStrings(value, result) {
311
+ if (typeof value === "string") {
312
+ result.push(value);
313
+ return;
314
+ }
315
+ if (Array.isArray(value)) {
316
+ value.forEach((item) => collectStrings(item, result));
317
+ return;
318
+ }
319
+ if (isRecord(value)) {
320
+ Object.values(value).forEach((item) => collectStrings(item, result));
321
+ }
322
+ }
323
+ function firstString(...values) {
324
+ return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim() ?? "";
325
+ }
326
+ function isRecord(value) {
327
+ return typeof value === "object" && value !== null && !Array.isArray(value);
328
+ }
329
+ function globMatches(glob, value) {
330
+ const source = glob
331
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
332
+ .replace(/\*\*/g, ".*")
333
+ .replace(/\*/g, "[^/]*");
334
+ return new RegExp(`^${source}$`).test(value.replace(/^\.\//, ""));
335
+ }
336
+ const STOP_WORDS = new Set([
337
+ "and", "the", "with", "for", "from", "into", "that", "this", "when", "then", "must", "should", "will", "are", "was",
338
+ "как", "для", "что", "это", "или", "при", "над", "под", "если",
339
+ ]);
@@ -15,7 +15,15 @@ export const STRUCTURED_ARTIFACT_SCHEMA_IDS = [
15
15
  "jira-description/v1",
16
16
  "mr-description/v1",
17
17
  "planning-questions/v1",
18
+ "playbook-answers/v1",
19
+ "playbook-draft/v1",
20
+ "playbook-final/v1",
21
+ "playbook-questions/v1",
22
+ "playbook-write-result/v1",
23
+ "practice-candidates/v1",
24
+ "project-guidance/v1",
18
25
  "qa-plan/v1",
26
+ "repo-inventory/v1",
19
27
  "review-assessment/v1",
20
28
  "review-findings/v1",
21
29
  "review-fix-report/v1",
@@ -1,4 +1,94 @@
1
1
  {
2
+ "project-guidance/v1": {
3
+ "type": "object",
4
+ "properties": {
5
+ "summary": { "type": "string", "nonEmpty": true },
6
+ "status": { "type": "string", "enum": ["available", "missing_playbook", "empty_selection", "invalid_playbook"] },
7
+ "phase": { "type": "string", "enum": ["plan", "design-review", "implement", "review", "repair/review-fix"] },
8
+ "source_playbook": {
9
+ "type": "object",
10
+ "properties": {
11
+ "root_dir": { "type": "string", "nonEmpty": true },
12
+ "manifest_path": { "type": "string", "nonEmpty": true },
13
+ "exists": { "type": "boolean" },
14
+ "valid": { "type": "boolean" },
15
+ "error": { "type": "string", "nonEmpty": true }
16
+ },
17
+ "required": ["root_dir", "manifest_path", "exists", "valid"]
18
+ },
19
+ "task_signals": {
20
+ "type": "object",
21
+ "properties": {
22
+ "title": { "type": "string", "nonEmpty": true },
23
+ "source_type": { "type": "string", "nonEmpty": true },
24
+ "keywords": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
25
+ "file_globs": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
26
+ "referenced_paths": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
27
+ "languages": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
28
+ "frameworks": { "type": "array", "items": { "type": "string", "nonEmpty": true } }
29
+ },
30
+ "required": ["title", "source_type", "keywords", "file_globs", "referenced_paths", "languages", "frameworks"]
31
+ },
32
+ "selected_practices": { "type": "array", "items": { "type": "object", "properties": {
33
+ "id": { "type": "string", "nonEmpty": true },
34
+ "title": { "type": "string", "nonEmpty": true },
35
+ "kind": { "type": "string", "enum": ["practice"] },
36
+ "source_path": { "type": "string", "nonEmpty": true },
37
+ "selection_reasons": { "type": "array", "items": { "type": "string", "nonEmpty": true }, "minItems": 1 },
38
+ "relevance_score": { "type": "number" },
39
+ "priority": { "type": "number" },
40
+ "severity": { "type": "string", "enum": ["must", "should", "info"] },
41
+ "reference_only": { "type": "boolean" },
42
+ "reference": { "type": "object", "properties": {
43
+ "source_path": { "type": "string", "nonEmpty": true },
44
+ "reason": { "type": "string", "nonEmpty": true }
45
+ }, "required": ["source_path", "reason"] },
46
+ "inline_content": { "type": "string", "nonEmpty": true }
47
+ }, "required": ["id", "title", "kind", "source_path", "selection_reasons", "relevance_score", "priority", "severity", "reference_only", "reference"] } },
48
+ "selected_examples": { "type": "array", "items": { "type": "object", "properties": {
49
+ "id": { "type": "string", "nonEmpty": true },
50
+ "title": { "type": "string", "nonEmpty": true },
51
+ "kind": { "type": "string", "enum": ["example"] },
52
+ "source_path": { "type": "string", "nonEmpty": true },
53
+ "selection_reasons": { "type": "array", "items": { "type": "string", "nonEmpty": true }, "minItems": 1 },
54
+ "relevance_score": { "type": "number" },
55
+ "priority": { "type": "number" },
56
+ "severity": { "type": "string", "enum": ["must", "should", "info"] },
57
+ "reference_only": { "type": "boolean" },
58
+ "reference": { "type": "object", "properties": {
59
+ "source_path": { "type": "string", "nonEmpty": true },
60
+ "reason": { "type": "string", "nonEmpty": true }
61
+ }, "required": ["source_path", "reason"] },
62
+ "inline_content": { "type": "string", "nonEmpty": true }
63
+ }, "required": ["id", "title", "kind", "source_path", "selection_reasons", "relevance_score", "priority", "severity", "reference_only", "reference"] } },
64
+ "always_include": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
65
+ "phase_sections": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
66
+ "budget": {
67
+ "type": "object",
68
+ "properties": {
69
+ "limit": { "type": "number", "minimum": 0 },
70
+ "used": { "type": "number", "minimum": 0 },
71
+ "remaining": { "type": "number", "minimum": 0 },
72
+ "inline_threshold": { "type": "number", "minimum": 0 },
73
+ "unit": { "type": "string", "enum": ["approx_tokens"] },
74
+ "estimator": { "type": "string", "enum": ["chars_div_4"] }
75
+ },
76
+ "required": ["limit", "used", "remaining", "inline_threshold", "unit", "estimator"]
77
+ },
78
+ "skipped_items": { "type": "array", "items": { "type": "object", "properties": {
79
+ "id": { "type": "string", "nonEmpty": true },
80
+ "kind": { "type": "string", "enum": ["practice", "example", "always_include"] },
81
+ "source_path": { "type": "string", "nonEmpty": true },
82
+ "reason": { "type": "string", "nonEmpty": true }
83
+ }, "required": ["id", "kind", "source_path", "reason"] } },
84
+ "warnings": { "type": "array", "items": { "type": "object", "properties": {
85
+ "code": { "type": "string", "nonEmpty": true },
86
+ "message": { "type": "string", "nonEmpty": true },
87
+ "severity": { "type": "string", "enum": ["info", "warning", "error"] }
88
+ }, "required": ["code", "message", "severity"] } }
89
+ },
90
+ "required": ["summary", "status", "phase", "source_playbook", "task_signals", "selected_practices", "selected_examples", "always_include", "phase_sections", "budget", "skipped_items", "warnings"]
91
+ },
2
92
  "bug-analysis/v1": {
3
93
  "type": "object",
4
94
  "properties": {
@@ -461,6 +551,151 @@
461
551
  },
462
552
  "required": ["summary", "questions"]
463
553
  },
554
+ "playbook-answers/v1": {
555
+ "type": "object",
556
+ "properties": {
557
+ "summary": { "type": "string", "nonEmpty": true },
558
+ "answered_at": { "type": "string", "nonEmpty": true },
559
+ "answers": {
560
+ "type": "array",
561
+ "items": {
562
+ "type": "object",
563
+ "properties": {
564
+ "question_id": { "type": "string", "nonEmpty": true },
565
+ "answer": { "type": "string" }
566
+ },
567
+ "required": ["question_id", "answer"]
568
+ }
569
+ },
570
+ "final_write_accepted": { "type": "boolean" }
571
+ },
572
+ "required": ["summary", "answered_at", "answers", "final_write_accepted"]
573
+ },
574
+ "playbook-draft/v1": {
575
+ "type": "object",
576
+ "properties": {
577
+ "summary": { "type": "string", "nonEmpty": true },
578
+ "generated_at": { "type": "string", "nonEmpty": true },
579
+ "accepted_rules": {
580
+ "type": "array",
581
+ "items": {
582
+ "type": "object",
583
+ "properties": {
584
+ "id": { "type": "string", "nonEmpty": true },
585
+ "title": { "type": "string", "nonEmpty": true },
586
+ "rule": { "type": "string", "nonEmpty": true },
587
+ "evidence_paths": { "type": "array", "items": { "type": "string", "nonEmpty": true }, "minItems": 1 }
588
+ },
589
+ "required": ["id", "title", "rule", "evidence_paths"]
590
+ }
591
+ },
592
+ "candidate_rules": { "type": "array", "items": { "type": "object" } },
593
+ "unresolved_questions": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
594
+ "evidence_paths": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
595
+ "proposed_files": { "type": "array", "items": { "type": "string", "nonEmpty": true }, "minItems": 2 }
596
+ },
597
+ "required": ["summary", "generated_at", "accepted_rules", "candidate_rules", "unresolved_questions", "evidence_paths", "proposed_files"]
598
+ },
599
+ "playbook-final/v1": {
600
+ "type": "object",
601
+ "properties": {
602
+ "status": { "type": "string", "nonEmpty": true, "enum": ["accepted"] },
603
+ "accepted_at": { "type": "string", "nonEmpty": true },
604
+ "source_draft_artifact": { "type": "string", "nonEmpty": true },
605
+ "summary": { "type": "string", "nonEmpty": true },
606
+ "rules": {
607
+ "type": "array",
608
+ "items": {
609
+ "type": "object",
610
+ "properties": {
611
+ "id": { "type": "string", "nonEmpty": true },
612
+ "title": { "type": "string", "nonEmpty": true },
613
+ "rule": { "type": "string", "nonEmpty": true },
614
+ "evidence_paths": { "type": "array", "items": { "type": "string", "nonEmpty": true }, "minItems": 1 }
615
+ },
616
+ "required": ["id", "title", "rule", "evidence_paths"]
617
+ }
618
+ },
619
+ "evidence_paths": { "type": "array", "items": { "type": "string", "nonEmpty": true } }
620
+ },
621
+ "required": ["status", "accepted_at", "source_draft_artifact", "summary", "rules", "evidence_paths"]
622
+ },
623
+ "playbook-questions/v1": {
624
+ "type": "object",
625
+ "properties": {
626
+ "summary": { "type": "string", "nonEmpty": true },
627
+ "questions": {
628
+ "type": "array",
629
+ "items": {
630
+ "type": "object",
631
+ "properties": {
632
+ "id": { "type": "string", "nonEmpty": true },
633
+ "text": { "type": "string", "nonEmpty": true },
634
+ "rationale": { "type": "string", "nonEmpty": true },
635
+ "candidate_ids": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
636
+ "evidence_paths": { "type": "array", "items": { "type": "string", "nonEmpty": true }, "minItems": 1 },
637
+ "answer_kind": { "type": "string", "nonEmpty": true, "enum": ["text", "boolean", "single_select"] }
638
+ },
639
+ "required": ["id", "text", "rationale", "candidate_ids", "evidence_paths", "answer_kind"]
640
+ }
641
+ }
642
+ },
643
+ "required": ["summary", "questions"]
644
+ },
645
+ "playbook-write-result/v1": {
646
+ "type": "object",
647
+ "properties": {
648
+ "status": { "type": "string", "nonEmpty": true, "enum": ["written", "skipped_valid_existing", "blocked", "not_accepted", "dry_run_written", "invalid_manifest", "partial_manifest", "missing_playbook", "failed"] },
649
+ "message": { "type": "string", "nonEmpty": true },
650
+ "written_files": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
651
+ "skipped_files": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
652
+ "existing_playbook_path": { "type": "string" },
653
+ "intended_files": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
654
+ "blocked_paths": { "type": "array", "items": { "type": "string", "nonEmpty": true } }
655
+ },
656
+ "required": ["status", "message", "written_files", "skipped_files", "existing_playbook_path", "intended_files", "blocked_paths"]
657
+ },
658
+ "practice-candidates/v1": {
659
+ "type": "object",
660
+ "properties": {
661
+ "summary": { "type": "string", "nonEmpty": true },
662
+ "candidates": {
663
+ "type": "array",
664
+ "items": {
665
+ "type": "object",
666
+ "properties": {
667
+ "id": { "type": "string", "nonEmpty": true },
668
+ "title": { "type": "string", "nonEmpty": true },
669
+ "proposed_rule_text": { "type": "string", "nonEmpty": true },
670
+ "confidence": { "type": "string", "nonEmpty": true, "enum": ["low", "medium", "high"] },
671
+ "evidence_paths": { "type": "array", "items": { "type": "string", "nonEmpty": true }, "minItems": 1 },
672
+ "rationale": { "type": "string", "nonEmpty": true },
673
+ "questions_needed": { "type": "array", "items": { "type": "string", "nonEmpty": true } }
674
+ },
675
+ "required": ["id", "title", "proposed_rule_text", "confidence", "evidence_paths", "rationale", "questions_needed"]
676
+ }
677
+ }
678
+ },
679
+ "required": ["summary", "candidates"]
680
+ },
681
+ "repo-inventory/v1": {
682
+ "type": "object",
683
+ "properties": {
684
+ "summary": { "type": "string", "nonEmpty": true },
685
+ "repository_root": { "type": "string", "nonEmpty": true },
686
+ "generated_at": { "type": "string", "nonEmpty": true },
687
+ "ignored_directories": { "type": "array", "items": { "type": "string", "nonEmpty": true } },
688
+ "stack_indicators": { "type": "array", "items": { "type": "object" } },
689
+ "test_structure": { "type": "array", "items": { "type": "object" } },
690
+ "architecture_hints": { "type": "array", "items": { "type": "object" } },
691
+ "quality_tooling": { "type": "array", "items": { "type": "object" } },
692
+ "specification_files": { "type": "array", "items": { "type": "object" } },
693
+ "runtime_configs": { "type": "array", "items": { "type": "object" } },
694
+ "generated_code": { "type": "array", "items": { "type": "object" } },
695
+ "evidence": { "type": "array", "items": { "type": "string", "nonEmpty": true } }
696
+ },
697
+ "required": ["summary", "repository_root", "generated_at", "ignored_directories", "stack_indicators", "test_structure", "architecture_hints", "quality_tooling", "specification_files", "runtime_configs", "generated_code", "evidence"]
698
+ },
464
699
  "qa-plan/v1": {
465
700
  "type": "object",
466
701
  "properties": {
@@ -75,7 +75,13 @@ function validateNode(value, schema, currentPath) {
75
75
  case "json":
76
76
  return validateJsonValue(value, currentPath);
77
77
  case "number":
78
- return typeof value === "number" && !Number.isNaN(value) ? [] : [`${currentPath} must be a number`];
78
+ if (typeof value !== "number" || Number.isNaN(value)) {
79
+ return [`${currentPath} must be a number`];
80
+ }
81
+ if (schema.minimum !== undefined && value < schema.minimum) {
82
+ return [`${currentPath} must be at least ${schema.minimum}`];
83
+ }
84
+ return [];
79
85
  case "null":
80
86
  return value === null ? [] : [`${currentPath} must be null`];
81
87
  case "array": {