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.
- package/Dockerfile.codex +55 -0
- package/README.md +11 -7
- package/dist/artifacts.js +64 -6
- package/dist/executors/configs/fetch-gitlab-review-config.js +3 -0
- package/dist/executors/fetch-gitlab-review-executor.js +25 -0
- package/dist/gitlab.js +153 -0
- package/dist/index.js +123 -98
- package/dist/interactive-ui.js +433 -28
- package/dist/pipeline/context.js +1 -0
- package/dist/pipeline/flow-specs/auto.json +176 -2
- package/dist/pipeline/flow-specs/gitlab-review.json +347 -0
- package/dist/pipeline/flow-specs/implement.json +12 -9
- package/dist/pipeline/flow-specs/mr-description.json +28 -0
- package/dist/pipeline/flow-specs/plan.json +52 -0
- package/dist/pipeline/flow-specs/preflight.json +32 -0
- package/dist/pipeline/flow-specs/review-fix.json +79 -10
- package/dist/pipeline/flow-specs/review.json +79 -0
- package/dist/pipeline/flow-specs/run-linter-loop.json +17 -11
- package/dist/pipeline/flow-specs/run-tests-loop.json +17 -11
- package/dist/pipeline/flow-specs/task-describe.json +29 -1
- package/dist/pipeline/node-registry.js +35 -0
- package/dist/pipeline/nodes/fetch-gitlab-review-node.js +34 -0
- package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +105 -0
- package/dist/pipeline/nodes/local-script-check-node.js +81 -0
- package/dist/pipeline/nodes/review-findings-form-node.js +65 -0
- package/dist/pipeline/nodes/user-input-node.js +93 -0
- package/dist/pipeline/prompt-registry.js +1 -3
- package/dist/pipeline/registry.js +2 -0
- package/dist/pipeline/value-resolver.js +37 -4
- package/dist/prompts.js +26 -17
- package/dist/structured-artifacts.js +208 -81
- package/dist/user-input.js +171 -0
- package/docker-compose.yml +384 -0
- package/package.json +7 -3
- package/run_linter.sh +89 -0
- package/run_tests.sh +113 -0
- package/verify_build.sh +104 -0
- package/dist/executors/claude-summary-executor.js +0 -31
- package/dist/executors/configs/claude-summary-config.js +0 -8
- package/dist/pipeline/flow-runner.js +0 -13
- package/dist/pipeline/flow-specs/test-fix.json +0 -24
- package/dist/pipeline/flow-specs/test-linter-fix.json +0 -24
- package/dist/pipeline/flow-specs/test.json +0 -19
- package/dist/pipeline/flow-types.js +0 -1
- package/dist/pipeline/flows/implement-flow.js +0 -47
- package/dist/pipeline/flows/plan-flow.js +0 -42
- package/dist/pipeline/flows/review-fix-flow.js +0 -62
- package/dist/pipeline/flows/review-flow.js +0 -124
- package/dist/pipeline/flows/test-fix-flow.js +0 -12
- package/dist/pipeline/flows/test-flow.js +0 -32
- package/dist/pipeline/nodes/claude-summary-node.js +0 -38
- package/dist/pipeline/nodes/implement-codex-node.js +0 -16
- 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
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"Разработай
|
|
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
|
-
"
|
|
26
|
-
export const IMPLEMENT_PROMPT_TEMPLATE = "
|
|
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
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
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 = "
|
|
50
|
+
export const REVIEW_FIX_PROMPT_TEMPLATE = "Используй только структурированные артефакты как source of truth. " +
|
|
51
|
+
"Проанализируй комментарии в {review_reply_json_file}. " +
|
|
41
52
|
"Исправь то, что содержится в дополнительных указаниях, а если таковых нет - исправь все пункты. " +
|
|
42
53
|
"По окончании обязательно прогони вне песочницы линтер, все тесты, сгенерируй make swagger. " +
|
|
43
54
|
"Исправь ошибки линтера и тестов, если будут. " +
|
|
44
|
-
"По завершении
|
|
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
|
|
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
|
|
23
|
-
if (!
|
|
24
|
-
issues.push(`${path} must be an
|
|
25
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
178
|
+
function reviewReplySchema() {
|
|
109
179
|
return {
|
|
110
|
-
id: "
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
+
}
|