agentweaver 0.1.16 → 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 (74) hide show
  1. package/README.md +148 -27
  2. package/dist/artifacts.js +114 -3
  3. package/dist/doctor/checks/executors.js +2 -2
  4. package/dist/flow-state.js +138 -1
  5. package/dist/index.js +421 -82
  6. package/dist/interactive/controller.js +305 -36
  7. package/dist/interactive/ink/index.js +24 -3
  8. package/dist/interactive/state.js +1 -0
  9. package/dist/interactive/tree.js +2 -2
  10. package/dist/interactive/web/index.js +179 -0
  11. package/dist/interactive/web/protocol.js +154 -0
  12. package/dist/interactive/web/server.js +575 -0
  13. package/dist/interactive/web/static/app.js +709 -0
  14. package/dist/interactive/web/static/index.html +77 -0
  15. package/dist/interactive/web/static/styles.css +2 -0
  16. package/dist/interactive/web/static/styles.input.css +469 -0
  17. package/dist/pipeline/auto-flow.js +9 -6
  18. package/dist/pipeline/context.js +6 -5
  19. package/dist/pipeline/declarative-flows.js +39 -20
  20. package/dist/pipeline/flow-catalog.js +40 -14
  21. package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
  22. package/dist/pipeline/flow-specs/auto-common.json +4 -1
  23. package/dist/pipeline/flow-specs/auto-golang.json +27 -1
  24. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +15 -1
  25. package/dist/pipeline/flow-specs/design-review.json +2 -0
  26. package/dist/pipeline/flow-specs/implement.json +3 -1
  27. package/dist/pipeline/flow-specs/plan.json +8 -2
  28. package/dist/pipeline/flow-specs/playbook-init.json +199 -0
  29. package/dist/pipeline/flow-specs/review/review-fix.json +3 -1
  30. package/dist/pipeline/flow-specs/review/review-loop.json +4 -0
  31. package/dist/pipeline/flow-specs/review/review.json +2 -0
  32. package/dist/pipeline/launch-profile-config.js +30 -18
  33. package/dist/pipeline/node-contract.js +1 -0
  34. package/dist/pipeline/node-registry.js +119 -5
  35. package/dist/pipeline/nodes/flow-run-node.js +200 -173
  36. package/dist/pipeline/nodes/llm-prompt-node.js +15 -33
  37. package/dist/pipeline/nodes/playbook-ensure-node.js +115 -0
  38. package/dist/pipeline/nodes/playbook-inventory-node.js +51 -0
  39. package/dist/pipeline/nodes/playbook-questions-form-node.js +166 -0
  40. package/dist/pipeline/nodes/playbook-write-node.js +243 -0
  41. package/dist/pipeline/nodes/project-guidance-node.js +69 -0
  42. package/dist/pipeline/plugin-loader.js +389 -0
  43. package/dist/pipeline/plugin-types.js +1 -0
  44. package/dist/pipeline/prompt-registry.js +4 -1
  45. package/dist/pipeline/prompt-runtime.js +6 -2
  46. package/dist/pipeline/registry.js +71 -4
  47. package/dist/pipeline/spec-compiler.js +1 -0
  48. package/dist/pipeline/spec-loader.js +14 -0
  49. package/dist/pipeline/spec-types.js +19 -0
  50. package/dist/pipeline/spec-validator.js +6 -0
  51. package/dist/pipeline/value-resolver.js +41 -2
  52. package/dist/playbook/practice-candidates.js +12 -0
  53. package/dist/playbook/repo-inventory.js +208 -0
  54. package/dist/plugin-sdk.js +1 -0
  55. package/dist/prompts.js +31 -0
  56. package/dist/runtime/artifact-registry.js +3 -0
  57. package/dist/runtime/execution-routing.js +25 -19
  58. package/dist/runtime/interactive-execution-routing.js +66 -57
  59. package/dist/runtime/playbook.js +485 -0
  60. package/dist/runtime/project-guidance.js +339 -0
  61. package/dist/structured-artifact-schema-registry.js +8 -0
  62. package/dist/structured-artifact-schemas.json +235 -0
  63. package/dist/structured-artifacts.js +7 -1
  64. package/docs/declarative-workflows.md +565 -0
  65. package/docs/example/.flows/examples/claude-example.json +50 -0
  66. package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
  67. package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
  68. package/docs/examples/.flows/claude-example.json +50 -0
  69. package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
  70. package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
  71. package/docs/features.md +77 -0
  72. package/docs/playbook.md +327 -0
  73. package/docs/plugin-sdk.md +731 -0
  74. package/package.json +13 -4
@@ -0,0 +1,166 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
4
+ import { TaskRunnerError } from "../../errors.js";
5
+ import { validateStructuredArtifactValue } from "../../structured-artifacts.js";
6
+ import { requestUserInputInTerminal } from "../../user-input.js";
7
+ function nowIso8601() {
8
+ return new Date().toISOString();
9
+ }
10
+ function readQuestions(filePath) {
11
+ if (!existsSync(filePath)) {
12
+ return [];
13
+ }
14
+ try {
15
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
16
+ return Array.isArray(parsed.questions) ? parsed.questions : [];
17
+ }
18
+ catch (error) {
19
+ throw new TaskRunnerError(`Failed to read playbook questions from ${filePath}: ${error.message}`);
20
+ }
21
+ }
22
+ function readExistingAnswers(filePath) {
23
+ if (!existsSync(filePath)) {
24
+ return null;
25
+ }
26
+ try {
27
+ return JSON.parse(readFileSync(filePath, "utf8"));
28
+ }
29
+ catch (error) {
30
+ throw new TaskRunnerError(`Failed to read playbook answers from ${filePath}: ${error.message}`);
31
+ }
32
+ }
33
+ function fieldForQuestion(question, index) {
34
+ const text = typeof question.text === "string" ? question.text.trim() : "";
35
+ if (!text) {
36
+ return null;
37
+ }
38
+ const id = typeof question.id === "string" && question.id.trim() ? question.id.trim() : `question_${index + 1}`;
39
+ return {
40
+ id,
41
+ type: "text",
42
+ label: text,
43
+ required: false,
44
+ multiline: true,
45
+ default: "",
46
+ ...(typeof question.rationale === "string" && question.rationale.trim() ? { help: question.rationale.trim() } : {}),
47
+ };
48
+ }
49
+ function answersFromValues(fields, values) {
50
+ return fields.map((field) => {
51
+ const value = values[field.id];
52
+ return {
53
+ question_id: field.id,
54
+ answer: typeof value === "string" ? value : value === undefined ? "" : JSON.stringify(value),
55
+ };
56
+ });
57
+ }
58
+ function writeAnswers(filePath, artifact) {
59
+ validateStructuredArtifactValue(artifact, "playbook-answers/v1", filePath);
60
+ mkdirSync(path.dirname(filePath), { recursive: true });
61
+ writeFileSync(filePath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
62
+ }
63
+ export const playbookQuestionsFormNode = {
64
+ kind: "playbook-questions-form",
65
+ version: 1,
66
+ async run(context, params) {
67
+ const mode = params.mode ?? "clarifications";
68
+ const existing = readExistingAnswers(params.answersJsonFile);
69
+ if (mode === "acceptance") {
70
+ let finalWriteAccepted = params.acceptDraft === true;
71
+ const interactive = context.requestUserInput !== requestUserInputInTerminal || (process.stdin.isTTY && process.stdout.isTTY);
72
+ if (params.acceptDraft !== true && interactive) {
73
+ const result = await (context.requestUserInput ?? requestUserInputInTerminal)({
74
+ formId: params.formId,
75
+ title: params.title,
76
+ submitLabel: "Confirm",
77
+ fields: [
78
+ {
79
+ id: "final_write_accepted",
80
+ type: "boolean",
81
+ label: "Write final .agentweaver/playbook files if safety checks pass?",
82
+ required: true,
83
+ default: false,
84
+ },
85
+ ],
86
+ });
87
+ finalWriteAccepted = result.values.final_write_accepted === true;
88
+ }
89
+ const artifact = {
90
+ summary: finalWriteAccepted ? "Final playbook write was accepted." : "Final playbook write was not accepted.",
91
+ answered_at: nowIso8601(),
92
+ answers: existing?.answers ?? [],
93
+ final_write_accepted: finalWriteAccepted,
94
+ };
95
+ writeAnswers(params.answersJsonFile, artifact);
96
+ return {
97
+ value: {
98
+ formId: params.formId,
99
+ questionCount: existing?.answers.length ?? 0,
100
+ finalWriteAccepted,
101
+ outputFile: params.answersJsonFile,
102
+ },
103
+ outputs: [
104
+ {
105
+ kind: "artifact",
106
+ path: params.answersJsonFile,
107
+ required: true,
108
+ manifest: {
109
+ publish: true,
110
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.answersJsonFile),
111
+ payloadFamily: "structured-json",
112
+ schemaId: "playbook-answers/v1",
113
+ schemaVersion: 1,
114
+ },
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ const fields = readQuestions(params.questionsJsonFile)
120
+ .map((question, index) => fieldForQuestion(question, index))
121
+ .filter((field) => field !== null);
122
+ let answers = [];
123
+ const interactive = fields.length === 0 || context.requestUserInput !== requestUserInputInTerminal || (process.stdin.isTTY && process.stdout.isTTY);
124
+ if (fields.length > 0 && !interactive) {
125
+ answers = fields.map((field) => ({ question_id: field.id, answer: "" }));
126
+ }
127
+ else if (fields.length > 0) {
128
+ const result = await (context.requestUserInput ?? requestUserInputInTerminal)({
129
+ formId: params.formId,
130
+ title: params.title,
131
+ submitLabel: "Continue",
132
+ fields,
133
+ });
134
+ answers = answersFromValues(fields, result.values);
135
+ }
136
+ const artifact = {
137
+ summary: fields.length === 0 ? "No clarification questions were required." : "Playbook clarification answers were recorded.",
138
+ answered_at: nowIso8601(),
139
+ answers,
140
+ final_write_accepted: params.acceptDraft === true,
141
+ };
142
+ writeAnswers(params.answersJsonFile, artifact);
143
+ return {
144
+ value: {
145
+ formId: params.formId,
146
+ questionCount: fields.length,
147
+ finalWriteAccepted: artifact.final_write_accepted,
148
+ outputFile: params.answersJsonFile,
149
+ },
150
+ outputs: [
151
+ {
152
+ kind: "artifact",
153
+ path: params.answersJsonFile,
154
+ required: true,
155
+ manifest: {
156
+ publish: true,
157
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.answersJsonFile),
158
+ payloadFamily: "structured-json",
159
+ schemaId: "playbook-answers/v1",
160
+ schemaVersion: 1,
161
+ },
162
+ },
163
+ ],
164
+ };
165
+ },
166
+ };
@@ -0,0 +1,243 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
4
+ import { TaskRunnerError } from "../../errors.js";
5
+ import { loadProjectPlaybook } from "../../runtime/playbook.js";
6
+ import { validateStructuredArtifact, validateStructuredArtifactValue } from "../../structured-artifacts.js";
7
+ function readJson(filePath) {
8
+ try {
9
+ return JSON.parse(readFileSync(filePath, "utf8"));
10
+ }
11
+ catch (error) {
12
+ throw new TaskRunnerError(`Failed to read JSON from ${filePath}: ${error.message}`);
13
+ }
14
+ }
15
+ function finalPlaybookPaths(cwd) {
16
+ const dir = path.join(cwd, ".agentweaver", "playbook");
17
+ return {
18
+ dir,
19
+ manifestFile: path.join(dir, "manifest.yaml"),
20
+ projectFile: path.join(dir, "project.md"),
21
+ practiceFile: path.join(dir, "practices", "generated-rules.md"),
22
+ exampleFile: path.join(dir, "examples", "generated-example.md"),
23
+ templateFile: path.join(dir, "templates", "default.md"),
24
+ };
25
+ }
26
+ function detectExistingState(cwd, manifestFile) {
27
+ if (!existsSync(manifestFile)) {
28
+ return { status: "absent", blockedPaths: [], message: "No manifest-based playbook exists." };
29
+ }
30
+ try {
31
+ loadProjectPlaybook(cwd);
32
+ return { status: "accepted", blockedPaths: [manifestFile], message: "A valid manifest-based playbook already exists." };
33
+ }
34
+ catch (error) {
35
+ return {
36
+ status: "blocked",
37
+ blockedPaths: [manifestFile],
38
+ message: `Existing manifest-based playbook is invalid or partial: ${error.message}`,
39
+ };
40
+ }
41
+ }
42
+ function toFinalPlaybook(draft, draftJsonFile) {
43
+ const evidence = Array.from(new Set([
44
+ ...(Array.isArray(draft.evidence_paths) ? draft.evidence_paths : []),
45
+ ...draft.accepted_rules.flatMap((rule) => rule.evidence_paths),
46
+ ])).sort((left, right) => left.localeCompare(right));
47
+ return {
48
+ status: "accepted",
49
+ accepted_at: new Date().toISOString(),
50
+ source_draft_artifact: draftJsonFile,
51
+ summary: draft.summary,
52
+ rules: draft.accepted_rules,
53
+ evidence_paths: evidence,
54
+ };
55
+ }
56
+ function renderFinalMarkdown(finalPlaybook) {
57
+ const rules = Array.isArray(finalPlaybook.rules) ? finalPlaybook.rules : [];
58
+ return [
59
+ "# Project playbook",
60
+ "",
61
+ String(finalPlaybook.summary ?? ""),
62
+ "",
63
+ "## Required rules",
64
+ ...(rules.length === 0
65
+ ? ["- No accepted required rules."]
66
+ : rules.map((rule) => `- ${rule.title}: ${rule.rule}\n Evidence: ${rule.evidence_paths.join(", ")}`)),
67
+ "",
68
+ ].join("\n");
69
+ }
70
+ function yamlStringArray(values) {
71
+ return Array.from(new Set(values.filter((value) => value.trim().length > 0))).sort();
72
+ }
73
+ function quotedYamlString(value) {
74
+ return JSON.stringify(value);
75
+ }
76
+ function yamlList(values, indent = "") {
77
+ return values.length > 0 ? values.map((value) => `${indent}- ${quotedYamlString(value)}`) : [`${indent}[]`];
78
+ }
79
+ function renderPracticeMarkdown(draft) {
80
+ const body = draft.accepted_rules.length === 0
81
+ ? "No accepted project rules were generated."
82
+ : draft.accepted_rules.map((rule) => `## ${rule.title}\n\n${rule.rule}\n\nEvidence: ${rule.evidence_paths.join(", ")}`).join("\n\n");
83
+ return [
84
+ "---",
85
+ 'id: "practice.generated-rules"',
86
+ 'title: "Generated project rules"',
87
+ "phases:",
88
+ ...yamlList(["plan", "design_review", "implement", "review", "repair"], " "),
89
+ "priority: 10",
90
+ 'severity: "must"',
91
+ "related_practices: []",
92
+ "related_examples: []",
93
+ "---",
94
+ "",
95
+ body,
96
+ "",
97
+ ].join("\n");
98
+ }
99
+ function renderExampleMarkdown() {
100
+ return [
101
+ "---",
102
+ 'id: "example.generated-guidance"',
103
+ 'title: "Generated guidance reference"',
104
+ "phases:",
105
+ ...yamlList(["plan", "design_review", "implement", "review", "repair"], " "),
106
+ "priority: 0",
107
+ 'severity: "info"',
108
+ "related_practices:",
109
+ ...yamlList(["practice.generated-rules"], " "),
110
+ "related_examples: []",
111
+ "---",
112
+ "",
113
+ "Use this entry as a reference marker for generated playbook guidance.",
114
+ "",
115
+ ].join("\n");
116
+ }
117
+ function renderManifest(draft) {
118
+ const evidence = yamlStringArray([
119
+ ...(Array.isArray(draft.evidence_paths) ? draft.evidence_paths : []),
120
+ ...draft.accepted_rules.flatMap((rule) => rule.evidence_paths),
121
+ ]);
122
+ return [
123
+ "version: 1",
124
+ "project:",
125
+ ' name: "Generated Project Playbook"',
126
+ "context_budgets:",
127
+ " plan: 1200",
128
+ " design_review: 1000",
129
+ " implement: 1400",
130
+ " review: 1000",
131
+ " repair: 1000",
132
+ "practices:",
133
+ " paths:",
134
+ ...yamlList(["practices/generated-rules.md"], " "),
135
+ " globs: []",
136
+ "examples:",
137
+ " paths:",
138
+ ...yamlList(["examples/generated-example.md"], " "),
139
+ " globs: []",
140
+ "templates:",
141
+ " paths:",
142
+ ...yamlList(["templates/default.md"], " "),
143
+ " globs: []",
144
+ "always_include:",
145
+ ...yamlList(["project.md"], " "),
146
+ "selection:",
147
+ " include_examples: true",
148
+ " max_examples: 1",
149
+ "evidence_paths:",
150
+ ...yamlList(evidence, " "),
151
+ "",
152
+ ].join("\n");
153
+ }
154
+ function writeResult(filePath, result) {
155
+ validateStructuredArtifactValue(result, "playbook-write-result/v1", filePath);
156
+ mkdirSync(path.dirname(filePath), { recursive: true });
157
+ writeFileSync(filePath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
158
+ }
159
+ function baseResult(status, message, intendedFiles) {
160
+ return {
161
+ status,
162
+ message,
163
+ written_files: [],
164
+ skipped_files: [],
165
+ existing_playbook_path: "",
166
+ intended_files: intendedFiles,
167
+ blocked_paths: [],
168
+ };
169
+ }
170
+ export const playbookWriteNode = {
171
+ kind: "playbook-write",
172
+ version: 1,
173
+ async run(context, params) {
174
+ const { dir, manifestFile, projectFile, practiceFile, exampleFile, templateFile } = finalPlaybookPaths(context.cwd);
175
+ const intendedFiles = [manifestFile, projectFile, practiceFile, exampleFile, templateFile];
176
+ const answers = existsSync(params.answersJsonFile) ? readJson(params.answersJsonFile) : {};
177
+ const accepted = answers.final_write_accepted === true;
178
+ let result;
179
+ if (!accepted) {
180
+ result = baseResult("not_accepted", "Final playbook write was not accepted in playbook-answers.json.", intendedFiles);
181
+ writeResult(params.writeResultJsonFile, result);
182
+ return { value: { ...result, finalJsonFile: manifestFile, finalMarkdownFile: projectFile, finalManifestFile: manifestFile }, outputs: outputSpecs(context.issueKey, params.writeResultJsonFile) };
183
+ }
184
+ if (context.dryRun) {
185
+ result = baseResult("dry_run_written", "Dry-run mode accepted the generated manifest layout but did not write final playbook files.", intendedFiles);
186
+ writeResult(params.writeResultJsonFile, result);
187
+ return { value: { ...result, finalJsonFile: manifestFile, finalMarkdownFile: projectFile, finalManifestFile: manifestFile }, outputs: outputSpecs(context.issueKey, params.writeResultJsonFile) };
188
+ }
189
+ const existing = detectExistingState(context.cwd, manifestFile);
190
+ if (existing.status === "accepted") {
191
+ result = {
192
+ ...baseResult("skipped_valid_existing", existing.message, intendedFiles),
193
+ skipped_files: intendedFiles,
194
+ existing_playbook_path: manifestFile,
195
+ };
196
+ writeResult(params.writeResultJsonFile, result);
197
+ return { value: { ...result, finalJsonFile: manifestFile, finalMarkdownFile: projectFile, finalManifestFile: manifestFile }, outputs: outputSpecs(context.issueKey, params.writeResultJsonFile) };
198
+ }
199
+ if (existing.status === "blocked") {
200
+ result = {
201
+ ...baseResult("blocked", existing.message, intendedFiles),
202
+ blocked_paths: existing.blockedPaths,
203
+ };
204
+ writeResult(params.writeResultJsonFile, result);
205
+ return { value: { ...result, finalJsonFile: manifestFile, finalMarkdownFile: projectFile, finalManifestFile: manifestFile }, outputs: outputSpecs(context.issueKey, params.writeResultJsonFile) };
206
+ }
207
+ validateStructuredArtifact(params.draftJsonFile, "playbook-draft/v1");
208
+ const draft = readJson(params.draftJsonFile);
209
+ const finalPlaybook = toFinalPlaybook(draft, params.draftJsonFile);
210
+ mkdirSync(dir, { recursive: true });
211
+ mkdirSync(path.dirname(practiceFile), { recursive: true });
212
+ mkdirSync(path.dirname(exampleFile), { recursive: true });
213
+ mkdirSync(path.dirname(templateFile), { recursive: true });
214
+ writeFileSync(manifestFile, renderManifest(draft), "utf8");
215
+ writeFileSync(projectFile, renderFinalMarkdown(finalPlaybook), "utf8");
216
+ writeFileSync(practiceFile, renderPracticeMarkdown(draft), "utf8");
217
+ writeFileSync(exampleFile, renderExampleMarkdown(), "utf8");
218
+ writeFileSync(templateFile, "# Default Template\n", "utf8");
219
+ loadProjectPlaybook(context.cwd);
220
+ result = {
221
+ ...baseResult("written", "Accepted playbook was written to the canonical manifest.yaml layout.", intendedFiles),
222
+ written_files: intendedFiles,
223
+ };
224
+ writeResult(params.writeResultJsonFile, result);
225
+ return { value: { ...result, finalJsonFile: manifestFile, finalMarkdownFile: projectFile, finalManifestFile: manifestFile }, outputs: outputSpecs(context.issueKey, params.writeResultJsonFile) };
226
+ },
227
+ };
228
+ function outputSpecs(scopeKey, writeResultJsonFile) {
229
+ return [
230
+ {
231
+ kind: "artifact",
232
+ path: writeResultJsonFile,
233
+ required: true,
234
+ manifest: {
235
+ publish: true,
236
+ logicalKey: buildLogicalKeyForPayload(scopeKey, writeResultJsonFile),
237
+ payloadFamily: "structured-json",
238
+ schemaId: "playbook-write-result/v1",
239
+ schemaVersion: 1,
240
+ },
241
+ },
242
+ ];
243
+ }
@@ -0,0 +1,69 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
4
+ import { TaskRunnerError } from "../../errors.js";
5
+ import { buildProjectGuidance, renderProjectGuidanceMarkdown } from "../../runtime/project-guidance.js";
6
+ import { validateStructuredArtifactValue } from "../../structured-artifacts.js";
7
+ function readTaskContext(filePath) {
8
+ try {
9
+ return JSON.parse(readFileSync(filePath, "utf8"));
10
+ }
11
+ catch (error) {
12
+ throw new TaskRunnerError(`Failed to read task context JSON ${filePath}: ${error.message}`);
13
+ }
14
+ }
15
+ export const projectGuidanceNode = {
16
+ kind: "project-guidance",
17
+ version: 1,
18
+ async run(context, params) {
19
+ const taskContext = readTaskContext(params.taskContextJsonFile);
20
+ const guidance = buildProjectGuidance({
21
+ projectRoot: context.cwd,
22
+ taskContext,
23
+ phase: params.phase,
24
+ ...(params.budgetLimit !== undefined ? { budgetLimit: params.budgetLimit } : {}),
25
+ ...(params.inlineThreshold !== undefined ? { inlineThreshold: params.inlineThreshold } : {}),
26
+ invalidPlaybookPolicy: params.invalidPlaybookPolicy ?? "fail_before_prompt",
27
+ });
28
+ validateStructuredArtifactValue(guidance, "project-guidance/v1", params.outputJsonFile);
29
+ mkdirSync(path.dirname(params.outputJsonFile), { recursive: true });
30
+ writeFileSync(params.outputJsonFile, `${JSON.stringify(guidance, null, 2)}\n`, "utf8");
31
+ const markdown = renderProjectGuidanceMarkdown(guidance, params.markdownLanguage ?? context.mdLang ?? "en");
32
+ mkdirSync(path.dirname(params.outputFile), { recursive: true });
33
+ writeFileSync(params.outputFile, markdown, "utf8");
34
+ return {
35
+ value: {
36
+ status: guidance.status,
37
+ phase: guidance.phase,
38
+ outputJsonFile: params.outputJsonFile,
39
+ outputFile: params.outputFile,
40
+ },
41
+ outputs: [
42
+ {
43
+ kind: "artifact",
44
+ path: params.outputJsonFile,
45
+ required: true,
46
+ manifest: {
47
+ publish: true,
48
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputJsonFile),
49
+ payloadFamily: "structured-json",
50
+ schemaId: "project-guidance/v1",
51
+ schemaVersion: 1,
52
+ },
53
+ },
54
+ {
55
+ kind: "artifact",
56
+ path: params.outputFile,
57
+ required: true,
58
+ manifest: {
59
+ publish: true,
60
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputFile),
61
+ payloadFamily: "markdown",
62
+ schemaId: "markdown/v1",
63
+ schemaVersion: 1,
64
+ },
65
+ },
66
+ ],
67
+ };
68
+ },
69
+ };