agentweaver 0.1.4 → 0.1.6

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 (53) hide show
  1. package/Dockerfile.codex +55 -0
  2. package/README.md +11 -7
  3. package/dist/artifacts.js +64 -6
  4. package/dist/executors/configs/fetch-gitlab-review-config.js +3 -0
  5. package/dist/executors/fetch-gitlab-review-executor.js +25 -0
  6. package/dist/gitlab.js +153 -0
  7. package/dist/index.js +123 -98
  8. package/dist/interactive-ui.js +433 -28
  9. package/dist/pipeline/context.js +1 -0
  10. package/dist/pipeline/flow-specs/auto.json +176 -2
  11. package/dist/pipeline/flow-specs/gitlab-review.json +347 -0
  12. package/dist/pipeline/flow-specs/implement.json +12 -9
  13. package/dist/pipeline/flow-specs/mr-description.json +28 -0
  14. package/dist/pipeline/flow-specs/plan.json +52 -0
  15. package/dist/pipeline/flow-specs/preflight.json +32 -0
  16. package/dist/pipeline/flow-specs/review-fix.json +79 -10
  17. package/dist/pipeline/flow-specs/review.json +79 -0
  18. package/dist/pipeline/flow-specs/run-linter-loop.json +17 -11
  19. package/dist/pipeline/flow-specs/run-tests-loop.json +17 -11
  20. package/dist/pipeline/flow-specs/task-describe.json +29 -1
  21. package/dist/pipeline/node-registry.js +35 -0
  22. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +34 -0
  23. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +105 -0
  24. package/dist/pipeline/nodes/local-script-check-node.js +81 -0
  25. package/dist/pipeline/nodes/review-findings-form-node.js +65 -0
  26. package/dist/pipeline/nodes/user-input-node.js +93 -0
  27. package/dist/pipeline/prompt-registry.js +1 -3
  28. package/dist/pipeline/registry.js +2 -0
  29. package/dist/pipeline/value-resolver.js +37 -4
  30. package/dist/prompts.js +26 -17
  31. package/dist/structured-artifacts.js +208 -81
  32. package/dist/user-input.js +171 -0
  33. package/docker-compose.yml +384 -0
  34. package/package.json +7 -3
  35. package/run_linter.sh +89 -0
  36. package/run_tests.sh +113 -0
  37. package/verify_build.sh +104 -0
  38. package/dist/executors/claude-summary-executor.js +0 -31
  39. package/dist/executors/configs/claude-summary-config.js +0 -8
  40. package/dist/pipeline/flow-runner.js +0 -13
  41. package/dist/pipeline/flow-specs/test-fix.json +0 -24
  42. package/dist/pipeline/flow-specs/test-linter-fix.json +0 -24
  43. package/dist/pipeline/flow-specs/test.json +0 -19
  44. package/dist/pipeline/flow-types.js +0 -1
  45. package/dist/pipeline/flows/implement-flow.js +0 -47
  46. package/dist/pipeline/flows/plan-flow.js +0 -42
  47. package/dist/pipeline/flows/review-fix-flow.js +0 -62
  48. package/dist/pipeline/flows/review-flow.js +0 -124
  49. package/dist/pipeline/flows/test-fix-flow.js +0 -12
  50. package/dist/pipeline/flows/test-flow.js +0 -32
  51. package/dist/pipeline/nodes/claude-summary-node.js +0 -38
  52. package/dist/pipeline/nodes/implement-codex-node.js +0 -16
  53. package/dist/pipeline/nodes/task-summary-node.js +0 -42
package/dist/prompts.js CHANGED
@@ -1,9 +1,15 @@
1
1
  export const BASE_PROMPT_HEADER = "Основная задача:";
2
2
  export const EXTRA_PROMPT_HEADER = "Дополнительные указания:";
3
3
  export const PLAN_PROMPT_TEMPLATE = "Посмотри и проанализируй задачу в {jira_task_file}. " +
4
- "Разработай системный дизайн решения, запиши в {design_file}. " +
5
- "Разработай подробный план реализации и запиши его в {plan_file}. " +
6
- "Разработай план тестирования для QA и запиши в {qa_file}. ";
4
+ "Сначала создай структурированные JSON-артефакты, они являются source of truth для следующих flow. " +
5
+ "Человекочитаемые markdown-файлы сделай как краткое производное представление этих JSON-артефактов для пользователя. " +
6
+ "Разработай системный дизайн решения и запиши JSON в {design_json_file}, затем markdown в {design_file}. " +
7
+ "Для {design_json_file} используй объект: { summary: string, goals: string[], non_goals: string[], components: string[], decisions: [{ component: string, decision: string, rationale: string }], risks: string[], open_questions: string[] }. " +
8
+ "Разработай подробный план реализации и запиши JSON в {plan_json_file}, затем markdown в {plan_file}. " +
9
+ "Для {plan_json_file} используй объект: { summary: string, prerequisites: string[], implementation_steps: [{ id: string, title: string, details: string }], tests: string[], rollout_notes: string[] }. " +
10
+ "Разработай план тестирования для QA и запиши JSON в {qa_json_file}, затем markdown в {qa_file}. " +
11
+ "Для {qa_json_file} используй объект: { summary: string, test_scenarios: [{ id: string, title: string, expected_result: string }], non_functional_checks: string[] }. " +
12
+ "JSON-файлы должны быть валидными и содержать только JSON без markdown-обёртки. ";
7
13
  export const BUG_ANALYZE_PROMPT_TEMPLATE = "Посмотри и проанализируй баг в {jira_task_file}. " +
8
14
  "Сначала создай структурированные JSON-артефакты, они являются source of truth для следующих flow. " +
9
15
  "Человекочитаемые markdown-файлы сделай как краткое производное представление этих JSON-артефактов для пользователя. " +
@@ -22,31 +28,34 @@ export const BUG_FIX_PROMPT_TEMPLATE = "Используй только стру
22
28
  "После этого приступай к реализации исправления в коде. ";
23
29
  export const MR_DESCRIPTION_PROMPT_TEMPLATE = "Посмотри задачу в {jira_task_file} и текущие изменения в репозитории. " +
24
30
  "Подготовь очень краткое intent-описание для merge request без подробностей реализации, списков файлов и технических деталей. " +
25
- "Сфокусируйся на том, что меняется и зачем. Запиши результат в {mr_description_file}. ";
26
- export const IMPLEMENT_PROMPT_TEMPLATE = "Проанализируй системный дизайн {design_file}, план реализации {plan_file} и приступай к реализации по плану. ";
31
+ "Сначала запиши source-of-truth JSON в {mr_description_json_file} в виде объекта { summary: string }, затем производную markdown-версию в {mr_description_file}. ";
32
+ export const IMPLEMENT_PROMPT_TEMPLATE = "Используй только структурированные артефакты как source of truth. " +
33
+ "Проанализируй системный дизайн {design_json_file}, план реализации {plan_json_file} и приступай к реализации по плану. " +
34
+ "Markdown-артефакты предназначены только для чтения человеком и не должны определять реализацию. ";
27
35
  export const REVIEW_PROMPT_TEMPLATE = "Проведи код-ревью текущих изменений. " +
28
- "Сверься с задачей в {jira_task_file}, дизайном {design_file} и планом {plan_file}. " +
29
- "Замечания и комментарии запиши в {review_file}. " +
30
- "Если больше нет блокеров, препятствующих merge - создай файл ready-to-merge.md.";
31
- export const REVIEW_REPLY_PROMPT_TEMPLATE = "Твой коллега провёл код-ревью и записал комментарии в {review_file}. " +
32
- "Проанализируй комментарии к код-ревью, сверься с задачей в {jira_task_file}, " +
33
- "дизайном {design_file}, планом {plan_file} и запиши свои комментарии в {review_reply_file}.";
36
+ "Используй только структурированные артефакты как source of truth: задачу в {jira_task_file}, дизайн в {design_json_file} и план в {plan_json_file}. " +
37
+ "Сначала запиши структурированный результат в {review_json_file} в виде объекта { summary: string, ready_to_merge: boolean, findings: [{ severity: string, title: string, description: string }] }. " +
38
+ "Затем запиши производную markdown-версию в {review_file}. " +
39
+ "Если ready_to_merge=true и нет блокеров, препятствующих merge - создай файл ready-to-merge.md.";
40
+ export const REVIEW_REPLY_PROMPT_TEMPLATE = "Твой коллега провёл код-ревью и записал структурированный результат в {review_json_file}. " +
41
+ "Используй только структурированные артефакты как source of truth: задачу в {jira_task_file}, дизайн в {design_json_file}, план в {plan_json_file} и review в {review_json_file}. " +
42
+ "Сначала запиши структурированный ответ в {review_reply_json_file} в виде объекта { summary: string, ready_to_merge: boolean, responses: [{ finding_title: string, disposition: string, action: string }] }. " +
43
+ "Затем запиши производную markdown-версию в {review_reply_file}.";
34
44
  export const REVIEW_SUMMARY_PROMPT_TEMPLATE = "Посмотри в {review_file}. " +
35
45
  "Сделай краткий список комментариев без подробностей, 3-7 пунктов. " +
36
46
  "Запиши результат в {review_summary_file}.";
37
47
  export const REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE = "Посмотри в {review_reply_file}. " +
38
48
  "Сделай краткий список ответов и итоговых действий без подробностей, 3-7 пунктов. " +
39
49
  "Запиши результат в {review_reply_summary_file}.";
40
- export const REVIEW_FIX_PROMPT_TEMPLATE = "Проанализируй комментарии в {review_reply_file}. " +
50
+ export const REVIEW_FIX_PROMPT_TEMPLATE = "Используй только структурированные артефакты как source of truth. " +
51
+ "Проанализируй комментарии в {review_reply_json_file}. " +
41
52
  "Исправь то, что содержится в дополнительных указаниях, а если таковых нет - исправь все пункты. " +
42
53
  "По окончании обязательно прогони вне песочницы линтер, все тесты, сгенерируй make swagger. " +
43
54
  "Исправь ошибки линтера и тестов, если будут. " +
44
- "По завершении резюме запиши в {review_fix_file}.";
55
+ "По завершении сначала запиши структурированный отчёт в {review_fix_json_file} в виде объекта { summary: string, completed_actions: string[], validation_steps: string[] }, затем производную markdown-версию в {review_fix_file}.";
45
56
  export const TASK_SUMMARY_PROMPT_TEMPLATE = "Посмотри в {jira_task_file}. " +
46
- "Сделай краткое резюме задачи, на 1-2 абзаца, " +
47
- "запиши в {task_summary_file}.";
48
- export const TEST_FIX_PROMPT_TEMPLATE = "Прогони тесты, исправь ошибки.";
49
- export const TEST_LINTER_FIX_PROMPT_TEMPLATE = "Прогони линтер, исправь замечания.";
57
+ "Сделай краткое резюме задачи, на 1-2 абзаца. " +
58
+ "Сначала запиши source-of-truth JSON в {task_summary_json_file} в виде объекта { summary: string }, затем markdown-версию в {task_summary_file}.";
50
59
  export const RUN_TESTS_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_tests.sh, проанализируй последнюю ошибку проверки, исправь код и подготовь изменения так, чтобы следующий прогон run_tests.sh прошёл успешно.";
51
60
  export const RUN_LINTER_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_linter.sh, проанализируй последнюю ошибку линтера или генерации, исправь код и подготовь изменения так, чтобы следующий прогон run_linter.sh прошёл успешно.";
52
61
  export const AUTO_REVIEW_FIX_EXTRA_PROMPT = "Исправлять только блокеры, критикалы и важные";
@@ -8,23 +8,119 @@ function expectNonEmptyString(value, path, issues) {
8
8
  issues.push(`${path} must be a non-empty string`);
9
9
  }
10
10
  }
11
- function expectStringArray(value, path, issues) {
11
+ function expectBoolean(value, path, issues) {
12
+ if (typeof value !== "boolean") {
13
+ issues.push(`${path} must be a boolean`);
14
+ }
15
+ }
16
+ function expectObject(value, path, issues) {
17
+ if (!isRecord(value)) {
18
+ issues.push(`${path} must be an object`);
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ function expectStringArray(value, path, issues, allowEmpty = false) {
12
24
  if (!Array.isArray(value)) {
13
25
  issues.push(`${path} must be an array`);
14
26
  return;
15
27
  }
16
- if (value.length === 0) {
28
+ if (!allowEmpty && value.length === 0) {
17
29
  issues.push(`${path} must not be empty`);
18
30
  return;
19
31
  }
20
32
  value.forEach((item, index) => expectNonEmptyString(item, `${path}[${index}]`, issues));
21
33
  }
22
- function expectObject(value, path, issues) {
23
- if (!isRecord(value)) {
24
- issues.push(`${path} must be an object`);
25
- return false;
34
+ function expectObjectArray(value, path, issues, validateItem, allowEmpty = false) {
35
+ if (!Array.isArray(value)) {
36
+ issues.push(`${path} must be an array`);
37
+ return;
26
38
  }
27
- return true;
39
+ if (!allowEmpty && value.length === 0) {
40
+ issues.push(`${path} must not be empty`);
41
+ return;
42
+ }
43
+ value.forEach((item, index) => {
44
+ const itemPath = `${path}[${index}]`;
45
+ if (!expectObject(item, itemPath, issues)) {
46
+ return;
47
+ }
48
+ validateItem(item, itemPath, issues);
49
+ });
50
+ }
51
+ function validateBriefText(value, path, issues) {
52
+ if (!expectObject(value, path, issues)) {
53
+ return;
54
+ }
55
+ expectNonEmptyString(value.summary, `${path}.summary`, issues);
56
+ }
57
+ function expectNumber(value, path, issues) {
58
+ if (typeof value !== "number" || Number.isNaN(value)) {
59
+ issues.push(`${path} must be a number`);
60
+ }
61
+ }
62
+ function implementationDesignSchema() {
63
+ return {
64
+ id: "implementation-design/v1",
65
+ validate({ path, value }) {
66
+ const issues = [];
67
+ if (!expectObject(value, path, issues)) {
68
+ return issues;
69
+ }
70
+ expectNonEmptyString(value.summary, `${path}.summary`, issues);
71
+ expectStringArray(value.goals, `${path}.goals`, issues);
72
+ expectStringArray(value.non_goals, `${path}.non_goals`, issues, true);
73
+ expectStringArray(value.components, `${path}.components`, issues);
74
+ expectObjectArray(value.decisions, `${path}.decisions`, issues, (item, itemPath, currentIssues) => {
75
+ expectNonEmptyString(item.component, `${itemPath}.component`, currentIssues);
76
+ expectNonEmptyString(item.decision, `${itemPath}.decision`, currentIssues);
77
+ expectNonEmptyString(item.rationale, `${itemPath}.rationale`, currentIssues);
78
+ });
79
+ expectStringArray(value.risks, `${path}.risks`, issues, true);
80
+ expectStringArray(value.open_questions, `${path}.open_questions`, issues, true);
81
+ return issues;
82
+ },
83
+ };
84
+ }
85
+ function implementationPlanSchema() {
86
+ return {
87
+ id: "implementation-plan/v1",
88
+ validate({ path, value }) {
89
+ const issues = [];
90
+ if (!expectObject(value, path, issues)) {
91
+ return issues;
92
+ }
93
+ expectNonEmptyString(value.summary, `${path}.summary`, issues);
94
+ expectStringArray(value.prerequisites, `${path}.prerequisites`, issues, true);
95
+ expectObjectArray(value.implementation_steps, `${path}.implementation_steps`, issues, (item, itemPath, currentIssues) => {
96
+ expectNonEmptyString(item.id, `${itemPath}.id`, currentIssues);
97
+ expectNonEmptyString(item.title, `${itemPath}.title`, currentIssues);
98
+ expectNonEmptyString(item.details, `${itemPath}.details`, currentIssues);
99
+ });
100
+ expectStringArray(value.tests, `${path}.tests`, issues);
101
+ expectStringArray(value.rollout_notes, `${path}.rollout_notes`, issues, true);
102
+ return issues;
103
+ },
104
+ };
105
+ }
106
+ function qaPlanSchema() {
107
+ return {
108
+ id: "qa-plan/v1",
109
+ validate({ path, value }) {
110
+ const issues = [];
111
+ if (!expectObject(value, path, issues)) {
112
+ return issues;
113
+ }
114
+ expectNonEmptyString(value.summary, `${path}.summary`, issues);
115
+ expectObjectArray(value.test_scenarios, `${path}.test_scenarios`, issues, (item, itemPath, currentIssues) => {
116
+ expectNonEmptyString(item.id, `${itemPath}.id`, currentIssues);
117
+ expectNonEmptyString(item.title, `${itemPath}.title`, currentIssues);
118
+ expectNonEmptyString(item.expected_result, `${itemPath}.expected_result`, currentIssues);
119
+ });
120
+ expectStringArray(value.non_functional_checks, `${path}.non_functional_checks`, issues, true);
121
+ return issues;
122
+ },
123
+ };
28
124
  }
29
125
  function bugAnalysisSchema() {
30
126
  return {
@@ -42,13 +138,8 @@ function bugAnalysisSchema() {
42
138
  expectStringArray(value.reproduction_steps, `${path}.reproduction_steps`, issues);
43
139
  expectStringArray(value.affected_components, `${path}.affected_components`, issues);
44
140
  expectStringArray(value.evidence, `${path}.evidence`, issues);
45
- expectStringArray(value.risks, `${path}.risks`, issues);
46
- if (!Array.isArray(value.open_questions)) {
47
- issues.push(`${path}.open_questions must be an array`);
48
- }
49
- else {
50
- value.open_questions.forEach((item, index) => expectNonEmptyString(item, `${path}.open_questions[${index}]`, issues));
51
- }
141
+ expectStringArray(value.risks, `${path}.risks`, issues, true);
142
+ expectStringArray(value.open_questions, `${path}.open_questions`, issues, true);
52
143
  return issues;
53
144
  },
54
145
  };
@@ -56,93 +147,106 @@ function bugAnalysisSchema() {
56
147
  function bugFixDesignSchema() {
57
148
  return {
58
149
  id: "bug-fix-design/v1",
150
+ validate: implementationDesignSchema().validate,
151
+ };
152
+ }
153
+ function bugFixPlanSchema() {
154
+ return {
155
+ id: "bug-fix-plan/v1",
156
+ validate: implementationPlanSchema().validate,
157
+ };
158
+ }
159
+ function reviewFindingsSchema() {
160
+ return {
161
+ id: "review-findings/v1",
59
162
  validate({ path, value }) {
60
163
  const issues = [];
61
164
  if (!expectObject(value, path, issues)) {
62
165
  return issues;
63
166
  }
64
167
  expectNonEmptyString(value.summary, `${path}.summary`, issues);
65
- expectStringArray(value.goals, `${path}.goals`, issues);
66
- if (!Array.isArray(value.non_goals)) {
67
- issues.push(`${path}.non_goals must be an array`);
68
- }
69
- else {
70
- value.non_goals.forEach((item, index) => expectNonEmptyString(item, `${path}.non_goals[${index}]`, issues));
71
- }
72
- expectStringArray(value.target_components, `${path}.target_components`, issues);
73
- if (!Array.isArray(value.proposed_changes)) {
74
- issues.push(`${path}.proposed_changes must be an array`);
75
- }
76
- else if (value.proposed_changes.length === 0) {
77
- issues.push(`${path}.proposed_changes must not be empty`);
78
- }
79
- else {
80
- value.proposed_changes.forEach((item, index) => {
81
- if (!expectObject(item, `${path}.proposed_changes[${index}]`, issues)) {
82
- return;
83
- }
84
- expectNonEmptyString(item.component, `${path}.proposed_changes[${index}].component`, issues);
85
- expectNonEmptyString(item.change, `${path}.proposed_changes[${index}].change`, issues);
86
- expectNonEmptyString(item.rationale, `${path}.proposed_changes[${index}].rationale`, issues);
87
- });
88
- }
89
- if (!Array.isArray(value.alternatives_considered)) {
90
- issues.push(`${path}.alternatives_considered must be an array`);
91
- }
92
- else {
93
- value.alternatives_considered.forEach((item, index) => {
94
- if (!expectObject(item, `${path}.alternatives_considered[${index}]`, issues)) {
95
- return;
96
- }
97
- expectNonEmptyString(item.option, `${path}.alternatives_considered[${index}].option`, issues);
98
- expectNonEmptyString(item.decision, `${path}.alternatives_considered[${index}].decision`, issues);
99
- expectNonEmptyString(item.rationale, `${path}.alternatives_considered[${index}].rationale`, issues);
100
- });
101
- }
102
- expectStringArray(value.risks, `${path}.risks`, issues);
103
- expectStringArray(value.validation_strategy, `${path}.validation_strategy`, issues);
168
+ expectBoolean(value.ready_to_merge, `${path}.ready_to_merge`, issues);
169
+ expectObjectArray(value.findings, `${path}.findings`, issues, (item, itemPath, currentIssues) => {
170
+ expectNonEmptyString(item.severity, `${itemPath}.severity`, currentIssues);
171
+ expectNonEmptyString(item.title, `${itemPath}.title`, currentIssues);
172
+ expectNonEmptyString(item.description, `${itemPath}.description`, currentIssues);
173
+ }, true);
104
174
  return issues;
105
175
  },
106
176
  };
107
177
  }
108
- function bugFixPlanSchema() {
178
+ function reviewReplySchema() {
109
179
  return {
110
- id: "bug-fix-plan/v1",
180
+ id: "review-reply/v1",
111
181
  validate({ path, value }) {
112
182
  const issues = [];
113
183
  if (!expectObject(value, path, issues)) {
114
184
  return issues;
115
185
  }
116
186
  expectNonEmptyString(value.summary, `${path}.summary`, issues);
117
- if (!Array.isArray(value.prerequisites)) {
118
- issues.push(`${path}.prerequisites must be an array`);
119
- }
120
- else {
121
- value.prerequisites.forEach((item, index) => expectNonEmptyString(item, `${path}.prerequisites[${index}]`, issues));
122
- }
123
- if (!Array.isArray(value.implementation_steps)) {
124
- issues.push(`${path}.implementation_steps must be an array`);
125
- }
126
- else if (value.implementation_steps.length === 0) {
127
- issues.push(`${path}.implementation_steps must not be empty`);
128
- }
129
- else {
130
- value.implementation_steps.forEach((item, index) => {
131
- if (!expectObject(item, `${path}.implementation_steps[${index}]`, issues)) {
132
- return;
133
- }
134
- expectNonEmptyString(item.id, `${path}.implementation_steps[${index}].id`, issues);
135
- expectNonEmptyString(item.title, `${path}.implementation_steps[${index}].title`, issues);
136
- expectNonEmptyString(item.details, `${path}.implementation_steps[${index}].details`, issues);
137
- });
187
+ expectObjectArray(value.responses, `${path}.responses`, issues, (item, itemPath, currentIssues) => {
188
+ expectNonEmptyString(item.finding_title, `${itemPath}.finding_title`, currentIssues);
189
+ expectNonEmptyString(item.disposition, `${itemPath}.disposition`, currentIssues);
190
+ expectNonEmptyString(item.action, `${itemPath}.action`, currentIssues);
191
+ }, true);
192
+ expectBoolean(value.ready_to_merge, `${path}.ready_to_merge`, issues);
193
+ return issues;
194
+ },
195
+ };
196
+ }
197
+ function reviewFixReportSchema() {
198
+ return {
199
+ id: "review-fix-report/v1",
200
+ validate({ path, value }) {
201
+ const issues = [];
202
+ if (!expectObject(value, path, issues)) {
203
+ return issues;
138
204
  }
139
- expectStringArray(value.tests, `${path}.tests`, issues);
140
- if (!Array.isArray(value.rollout_notes)) {
141
- issues.push(`${path}.rollout_notes must be an array`);
205
+ expectNonEmptyString(value.summary, `${path}.summary`, issues);
206
+ expectStringArray(value.completed_actions, `${path}.completed_actions`, issues);
207
+ expectStringArray(value.validation_steps, `${path}.validation_steps`, issues, true);
208
+ return issues;
209
+ },
210
+ };
211
+ }
212
+ function gitlabReviewSchema() {
213
+ return {
214
+ id: "gitlab-review/v1",
215
+ validate({ path, value }) {
216
+ const issues = [];
217
+ if (!expectObject(value, path, issues)) {
218
+ return issues;
142
219
  }
143
- else {
144
- value.rollout_notes.forEach((item, index) => expectNonEmptyString(item, `${path}.rollout_notes[${index}]`, issues));
220
+ expectNonEmptyString(value.summary, `${path}.summary`, issues);
221
+ expectNonEmptyString(value.merge_request_url, `${path}.merge_request_url`, issues);
222
+ expectNonEmptyString(value.project_path, `${path}.project_path`, issues);
223
+ expectNumber(value.merge_request_iid, `${path}.merge_request_iid`, issues);
224
+ expectNonEmptyString(value.fetched_at, `${path}.fetched_at`, issues);
225
+ expectObjectArray(value.comments, `${path}.comments`, issues, (item, itemPath, currentIssues) => {
226
+ expectNonEmptyString(item.id, `${itemPath}.id`, currentIssues);
227
+ expectNonEmptyString(item.discussion_id, `${itemPath}.discussion_id`, currentIssues);
228
+ expectNonEmptyString(item.body, `${itemPath}.body`, currentIssues);
229
+ expectNonEmptyString(item.author, `${itemPath}.author`, currentIssues);
230
+ expectNonEmptyString(item.created_at, `${itemPath}.created_at`, currentIssues);
231
+ if (item.file_path !== undefined && item.file_path !== null) {
232
+ expectNonEmptyString(item.file_path, `${itemPath}.file_path`, currentIssues);
233
+ }
234
+ }, true);
235
+ return issues;
236
+ },
237
+ };
238
+ }
239
+ function userInputSchema() {
240
+ return {
241
+ id: "user-input/v1",
242
+ validate({ path, value }) {
243
+ const issues = [];
244
+ if (!expectObject(value, path, issues)) {
245
+ return issues;
145
246
  }
247
+ expectNonEmptyString(value.form_id, `${path}.form_id`, issues);
248
+ expectNonEmptyString(value.submitted_at, `${path}.submitted_at`, issues);
249
+ expectObject(value.values, `${path}.values`, issues);
146
250
  return issues;
147
251
  },
148
252
  };
@@ -151,6 +255,29 @@ const schemas = {
151
255
  "bug-analysis/v1": bugAnalysisSchema(),
152
256
  "bug-fix-design/v1": bugFixDesignSchema(),
153
257
  "bug-fix-plan/v1": bugFixPlanSchema(),
258
+ "gitlab-review/v1": gitlabReviewSchema(),
259
+ "implementation-design/v1": implementationDesignSchema(),
260
+ "implementation-plan/v1": implementationPlanSchema(),
261
+ "jira-description/v1": { id: "jira-description/v1", validate: ({ path, value }) => {
262
+ const issues = [];
263
+ validateBriefText(value, path, issues);
264
+ return issues;
265
+ } },
266
+ "mr-description/v1": { id: "mr-description/v1", validate: ({ path, value }) => {
267
+ const issues = [];
268
+ validateBriefText(value, path, issues);
269
+ return issues;
270
+ } },
271
+ "qa-plan/v1": qaPlanSchema(),
272
+ "review-findings/v1": reviewFindingsSchema(),
273
+ "review-fix-report/v1": reviewFixReportSchema(),
274
+ "review-reply/v1": reviewReplySchema(),
275
+ "task-summary/v1": { id: "task-summary/v1", validate: ({ path, value }) => {
276
+ const issues = [];
277
+ validateBriefText(value, path, issues);
278
+ return issues;
279
+ } },
280
+ "user-input/v1": userInputSchema(),
154
281
  };
155
282
  export function validateStructuredArtifact(path, schemaId) {
156
283
  if (!existsSync(path)) {
@@ -0,0 +1,171 @@
1
+ import process from "node:process";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { TaskRunnerError } from "./errors.js";
4
+ function normalizeText(value) {
5
+ return value.trim();
6
+ }
7
+ export function defaultValueForField(field) {
8
+ if (field.type === "boolean") {
9
+ return field.default ?? false;
10
+ }
11
+ if (field.type === "text") {
12
+ return field.default ?? "";
13
+ }
14
+ if (field.type === "single-select") {
15
+ return field.default ?? field.options[0]?.value ?? "";
16
+ }
17
+ return [...(field.default ?? [])];
18
+ }
19
+ export function buildInitialUserInputValues(fields) {
20
+ return Object.fromEntries(fields.map((field) => [field.id, defaultValueForField(field)]));
21
+ }
22
+ export function validateUserInputValues(form, values) {
23
+ for (const field of form.fields) {
24
+ const value = values[field.id];
25
+ if (field.type === "boolean") {
26
+ if (typeof value !== "boolean") {
27
+ throw new TaskRunnerError(`Field '${field.label}' must be a boolean.`);
28
+ }
29
+ continue;
30
+ }
31
+ if (field.type === "text") {
32
+ if (typeof value !== "string") {
33
+ throw new TaskRunnerError(`Field '${field.label}' must be a string.`);
34
+ }
35
+ if (field.required && normalizeText(value).length === 0) {
36
+ throw new TaskRunnerError(`Field '${field.label}' is required.`);
37
+ }
38
+ continue;
39
+ }
40
+ if (field.type === "single-select") {
41
+ if (typeof value !== "string") {
42
+ throw new TaskRunnerError(`Field '${field.label}' must be a string.`);
43
+ }
44
+ if (field.required && normalizeText(value).length === 0) {
45
+ throw new TaskRunnerError(`Field '${field.label}' is required.`);
46
+ }
47
+ if (value && !field.options.some((option) => option.value === value)) {
48
+ throw new TaskRunnerError(`Field '${field.label}' contains an unknown option '${value}'.`);
49
+ }
50
+ continue;
51
+ }
52
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
53
+ throw new TaskRunnerError(`Field '${field.label}' must be a string array.`);
54
+ }
55
+ if (field.required && value.length === 0) {
56
+ throw new TaskRunnerError(`Field '${field.label}' requires at least one selected option.`);
57
+ }
58
+ const allowed = new Set(field.options.map((option) => option.value));
59
+ for (const item of value) {
60
+ if (!allowed.has(item)) {
61
+ throw new TaskRunnerError(`Field '${field.label}' contains an unknown option '${item}'.`);
62
+ }
63
+ }
64
+ }
65
+ if (form.formId === "review-fix-selection") {
66
+ const applyAll = values.apply_all === true;
67
+ const selectedFindings = Array.isArray(values.selected_findings)
68
+ ? values.selected_findings.filter((item) => typeof item === "string" && item.trim().length > 0)
69
+ : [];
70
+ if (!applyAll && selectedFindings.length === 0) {
71
+ throw new TaskRunnerError("Select at least one finding or enable 'apply all'.");
72
+ }
73
+ }
74
+ }
75
+ function parseBoolean(value) {
76
+ const normalized = normalizeText(value).toLowerCase();
77
+ if (["y", "yes", "true", "1", "да", "д"].includes(normalized)) {
78
+ return true;
79
+ }
80
+ if (["n", "no", "false", "0", "нет", "н"].includes(normalized)) {
81
+ return false;
82
+ }
83
+ return null;
84
+ }
85
+ export async function requestUserInputInTerminal(form) {
86
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
87
+ throw new TaskRunnerError(`Flow requires interactive user input for form '${form.formId}', but no TTY is available.`);
88
+ }
89
+ const rl = createInterface({
90
+ input: process.stdin,
91
+ output: process.stdout,
92
+ });
93
+ try {
94
+ process.stdout.write(`\n${form.title}\n`);
95
+ if (form.description?.trim()) {
96
+ process.stdout.write(`${form.description.trim()}\n`);
97
+ }
98
+ const values = buildInitialUserInputValues(form.fields);
99
+ for (const field of form.fields) {
100
+ if (field.type === "boolean") {
101
+ while (true) {
102
+ const current = values[field.id];
103
+ const answer = await rl.question(`${field.label} [y/n] (${current ? "y" : "n"}): `);
104
+ const parsed = answer.trim() ? parseBoolean(answer) : Boolean(current);
105
+ if (parsed === null) {
106
+ process.stdout.write("Please answer y/n.\n");
107
+ continue;
108
+ }
109
+ values[field.id] = parsed;
110
+ break;
111
+ }
112
+ continue;
113
+ }
114
+ if (field.type === "text") {
115
+ const current = String(values[field.id] ?? "");
116
+ const answer = await rl.question(`${field.label}${current ? ` (${current})` : ""}: `);
117
+ values[field.id] = answer.trim() ? answer : current;
118
+ continue;
119
+ }
120
+ const options = field.options.map((option, index) => `${index + 1}. ${option.label}`).join("\n");
121
+ process.stdout.write(`${field.label}\n${options}\n`);
122
+ if (field.type === "single-select") {
123
+ while (true) {
124
+ const current = String(values[field.id] ?? "");
125
+ const answer = await rl.question(`Choose one option${current ? ` (${current})` : ""}: `);
126
+ const raw = answer.trim();
127
+ if (!raw && current) {
128
+ break;
129
+ }
130
+ const index = Number.parseInt(raw, 10) - 1;
131
+ const option = field.options[index];
132
+ if (!option) {
133
+ process.stdout.write("Unknown option number.\n");
134
+ continue;
135
+ }
136
+ values[field.id] = option.value;
137
+ break;
138
+ }
139
+ continue;
140
+ }
141
+ while (true) {
142
+ const current = Array.isArray(values[field.id]) ? values[field.id] : [];
143
+ const answer = await rl.question(`Choose one or more options separated by comma${current.length > 0 ? ` (${current.join(", ")})` : ""}: `);
144
+ const raw = answer.trim();
145
+ if (!raw && current.length > 0) {
146
+ break;
147
+ }
148
+ const selected = raw
149
+ .split(",")
150
+ .map((item) => Number.parseInt(item.trim(), 10) - 1)
151
+ .map((index) => field.options[index]?.value)
152
+ .filter((item) => Boolean(item));
153
+ if (selected.length === 0 && field.required) {
154
+ process.stdout.write("Select at least one option.\n");
155
+ continue;
156
+ }
157
+ values[field.id] = selected;
158
+ break;
159
+ }
160
+ }
161
+ validateUserInputValues(form, values);
162
+ return {
163
+ formId: form.formId,
164
+ submittedAt: new Date().toISOString(),
165
+ values,
166
+ };
167
+ }
168
+ finally {
169
+ rl.close();
170
+ }
171
+ }