agentweaver 0.1.5 → 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 (44) hide show
  1. package/Dockerfile.codex +55 -0
  2. package/README.md +10 -6
  3. package/dist/artifacts.js +9 -0
  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 +41 -91
  8. package/dist/pipeline/flow-specs/auto.json +2 -2
  9. package/dist/pipeline/flow-specs/gitlab-review.json +347 -0
  10. package/dist/pipeline/flow-specs/implement.json +0 -9
  11. package/dist/pipeline/flow-specs/review-fix.json +2 -11
  12. package/dist/pipeline/flow-specs/run-linter-loop.json +17 -11
  13. package/dist/pipeline/flow-specs/run-tests-loop.json +17 -11
  14. package/dist/pipeline/node-registry.js +20 -1
  15. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +34 -0
  16. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +105 -0
  17. package/dist/pipeline/nodes/local-script-check-node.js +81 -0
  18. package/dist/pipeline/nodes/review-findings-form-node.js +14 -14
  19. package/dist/pipeline/prompt-registry.js +1 -3
  20. package/dist/pipeline/registry.js +2 -0
  21. package/dist/pipeline/value-resolver.js +7 -1
  22. package/dist/prompts.js +0 -2
  23. package/dist/structured-artifacts.js +33 -0
  24. package/docker-compose.yml +384 -0
  25. package/package.json +7 -3
  26. package/run_linter.sh +89 -0
  27. package/run_tests.sh +113 -0
  28. package/verify_build.sh +104 -0
  29. package/dist/executors/claude-summary-executor.js +0 -31
  30. package/dist/executors/configs/claude-summary-config.js +0 -8
  31. package/dist/pipeline/flow-runner.js +0 -13
  32. package/dist/pipeline/flow-specs/test-fix.json +0 -24
  33. package/dist/pipeline/flow-specs/test-linter-fix.json +0 -24
  34. package/dist/pipeline/flow-specs/test.json +0 -19
  35. package/dist/pipeline/flow-types.js +0 -1
  36. package/dist/pipeline/flows/implement-flow.js +0 -47
  37. package/dist/pipeline/flows/plan-flow.js +0 -42
  38. package/dist/pipeline/flows/review-fix-flow.js +0 -62
  39. package/dist/pipeline/flows/review-flow.js +0 -124
  40. package/dist/pipeline/flows/test-fix-flow.js +0 -12
  41. package/dist/pipeline/flows/test-flow.js +0 -32
  42. package/dist/pipeline/nodes/claude-summary-node.js +0 -38
  43. package/dist/pipeline/nodes/implement-codex-node.js +0 -16
  44. package/dist/pipeline/nodes/task-summary-node.js +0 -42
@@ -0,0 +1,105 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { readFileSync } from "node:fs";
3
+ import { TaskRunnerError } from "../../errors.js";
4
+ function commentLine(comment) {
5
+ if (typeof comment.new_line === "number") {
6
+ return comment.new_line;
7
+ }
8
+ if (typeof comment.old_line === "number") {
9
+ return comment.old_line;
10
+ }
11
+ return null;
12
+ }
13
+ function commentLocation(comment) {
14
+ const filePath = typeof comment.file_path === "string" ? comment.file_path.trim() : "";
15
+ const line = commentLine(comment);
16
+ if (!filePath) {
17
+ return "general";
18
+ }
19
+ return line === null ? filePath : `${filePath}:${line}`;
20
+ }
21
+ function toReviewFinding(comment, index) {
22
+ const body = typeof comment.body === "string" ? comment.body.trim() : "";
23
+ if (!body) {
24
+ return null;
25
+ }
26
+ const author = typeof comment.author === "string" ? comment.author.trim() : "unknown";
27
+ const location = commentLocation(comment);
28
+ const preview = body.replace(/\s+/g, " ");
29
+ const titlePreview = preview.length > 72 ? `${preview.slice(0, 69)}...` : preview;
30
+ const discussionId = typeof comment.discussion_id === "string" ? comment.discussion_id.trim() : "";
31
+ const createdAt = typeof comment.created_at === "string" ? comment.created_at.trim() : "";
32
+ return {
33
+ severity: "medium",
34
+ title: `GitLab comment ${index + 1} | ${location} | ${author} | ${titlePreview}`,
35
+ description: [
36
+ `Location: ${location}`,
37
+ `Author: ${author}`,
38
+ discussionId ? `Discussion: ${discussionId}` : "",
39
+ createdAt ? `Created at: ${createdAt}` : "",
40
+ "",
41
+ body,
42
+ ]
43
+ .filter((line) => line.length > 0)
44
+ .join("\n"),
45
+ };
46
+ }
47
+ function renderReviewMarkdown(artifact) {
48
+ const lines = [
49
+ "# Review",
50
+ "",
51
+ artifact.summary,
52
+ "",
53
+ `Ready to merge: ${artifact.ready_to_merge ? "yes" : "no"}`,
54
+ "",
55
+ ];
56
+ if (artifact.findings.length === 0) {
57
+ lines.push("Замечаний не найдено.");
58
+ return lines.join("\n");
59
+ }
60
+ artifact.findings.forEach((finding, index) => {
61
+ lines.push(`## Finding ${index + 1}`);
62
+ lines.push(`- Severity: ${finding.severity}`);
63
+ lines.push(`- Title: ${finding.title}`);
64
+ lines.push("");
65
+ lines.push(finding.description);
66
+ lines.push("");
67
+ });
68
+ return lines.join("\n");
69
+ }
70
+ export const gitlabReviewArtifactsNode = {
71
+ kind: "gitlab-review-artifacts",
72
+ version: 1,
73
+ async run(_context, params) {
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(readFileSync(params.gitlabReviewJsonFile, "utf8"));
77
+ }
78
+ catch (error) {
79
+ throw new TaskRunnerError(`Failed to read GitLab review artifact from ${params.gitlabReviewJsonFile}: ${error.message}`);
80
+ }
81
+ const gitlabReview = parsed;
82
+ const findings = (Array.isArray(gitlabReview.comments) ? gitlabReview.comments : [])
83
+ .map((comment, index) => toReviewFinding(comment, index))
84
+ .filter((finding) => finding !== null);
85
+ const artifact = {
86
+ summary: findings.length > 0
87
+ ? `Импортировано ${findings.length} комментариев код-ревью из GitLab.`
88
+ : "Комментарии код-ревью в GitLab не найдены.",
89
+ ready_to_merge: findings.length === 0,
90
+ findings,
91
+ };
92
+ writeFileSync(params.reviewJsonFile, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
93
+ writeFileSync(params.reviewFile, `${renderReviewMarkdown(artifact)}\n`, "utf8");
94
+ return {
95
+ value: {
96
+ findingsCount: findings.length,
97
+ readyToMerge: artifact.ready_to_merge,
98
+ },
99
+ outputs: [
100
+ { kind: "artifact", path: params.reviewFile, required: true },
101
+ { kind: "artifact", path: params.reviewJsonFile, required: true },
102
+ ],
103
+ };
104
+ },
105
+ };
@@ -0,0 +1,81 @@
1
+ import { TaskRunnerError } from "../../errors.js";
2
+ import { printInfo } from "../../tui.js";
3
+ function parseStructuredResult(output, commandLabel) {
4
+ const lines = output
5
+ .split(/\r?\n/)
6
+ .map((line) => line.replace(/\u001b\[[0-9;]*m/g, "").trim())
7
+ .filter(Boolean);
8
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
9
+ const line = lines[index];
10
+ if (!line) {
11
+ continue;
12
+ }
13
+ const candidates = [];
14
+ if (line.startsWith("{") && line.endsWith("}")) {
15
+ candidates.push(line);
16
+ }
17
+ const firstBrace = line.indexOf("{");
18
+ const lastBrace = line.lastIndexOf("}");
19
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
20
+ const slice = line.slice(firstBrace, lastBrace + 1).trim();
21
+ if (slice && !candidates.includes(slice)) {
22
+ candidates.push(slice);
23
+ }
24
+ }
25
+ for (const rawJson of candidates) {
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(rawJson);
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
34
+ continue;
35
+ }
36
+ const candidate = parsed;
37
+ if (typeof candidate.ok !== "boolean" ||
38
+ typeof candidate.kind !== "string" ||
39
+ typeof candidate.stage !== "string" ||
40
+ typeof candidate.exitCode !== "number" ||
41
+ typeof candidate.summary !== "string" ||
42
+ typeof candidate.command !== "string") {
43
+ continue;
44
+ }
45
+ const details = candidate.details;
46
+ if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
47
+ continue;
48
+ }
49
+ return {
50
+ ok: candidate.ok,
51
+ kind: candidate.kind,
52
+ stage: candidate.stage,
53
+ exitCode: candidate.exitCode,
54
+ summary: candidate.summary,
55
+ command: candidate.command,
56
+ details: details ?? {},
57
+ };
58
+ }
59
+ }
60
+ throw new TaskRunnerError(`Structured result is missing or invalid in output of '${commandLabel}'.`);
61
+ }
62
+ export const localScriptCheckNode = {
63
+ kind: "local-script-check",
64
+ version: 1,
65
+ async run(context, params) {
66
+ printInfo(params.labelText);
67
+ const output = await context.runtime.runCommand(params.argv, {
68
+ dryRun: context.dryRun,
69
+ verbose: context.verbose,
70
+ label: params.argv.join(" "),
71
+ printFailureOutput: true,
72
+ env: { ...context.env },
73
+ });
74
+ return {
75
+ value: {
76
+ output,
77
+ parsed: parseStructuredResult(output, params.argv.join(" ")),
78
+ },
79
+ };
80
+ },
81
+ };
@@ -6,20 +6,20 @@ export const reviewFindingsFormNode = {
6
6
  async run(_context, params) {
7
7
  let parsed;
8
8
  try {
9
- parsed = JSON.parse(readFileSync(params.reviewJsonFile, "utf8"));
9
+ parsed = JSON.parse(readFileSync(params.reviewReplyJsonFile, "utf8"));
10
10
  }
11
11
  catch (error) {
12
- throw new TaskRunnerError(`Failed to read review findings from ${params.reviewJsonFile}: ${error.message}`);
12
+ throw new TaskRunnerError(`Failed to read review reply from ${params.reviewReplyJsonFile}: ${error.message}`);
13
13
  }
14
- const review = parsed;
15
- const findings = Array.isArray(review.findings) ? review.findings : [];
16
- const selectableFindings = findings
17
- .map((finding) => ({
18
- severity: typeof finding.severity === "string" ? finding.severity.trim() : "",
19
- title: typeof finding.title === "string" ? finding.title.trim() : "",
20
- description: typeof finding.description === "string" ? finding.description.trim() : "",
14
+ const reviewReply = parsed;
15
+ const responses = Array.isArray(reviewReply.responses) ? reviewReply.responses : [];
16
+ const selectableFindings = responses
17
+ .map((response) => ({
18
+ findingTitle: typeof response.finding_title === "string" ? response.finding_title.trim() : "",
19
+ disposition: typeof response.disposition === "string" ? response.disposition.trim() : "",
20
+ action: typeof response.action === "string" ? response.action.trim() : "",
21
21
  }))
22
- .filter((finding) => finding.title.length > 0);
22
+ .filter((response) => response.findingTitle.length > 0);
23
23
  const fields = [
24
24
  {
25
25
  id: "apply_all",
@@ -35,10 +35,10 @@ export const reviewFindingsFormNode = {
35
35
  type: "multi-select",
36
36
  label: "Какие findings исправить сейчас",
37
37
  help: "Space переключает пункт. Если apply_all=false, выберите хотя бы один finding.",
38
- options: selectableFindings.map((finding) => ({
39
- value: finding.title,
40
- label: `[${finding.severity || "info"}] ${finding.title}`,
41
- ...(finding.description ? { description: finding.description } : {}),
38
+ options: selectableFindings.map((response) => ({
39
+ value: response.findingTitle,
40
+ label: `${response.findingTitle} | ${response.disposition || "-"}`,
41
+ ...(response.action ? { description: response.action } : {}),
42
42
  })),
43
43
  default: [],
44
44
  });
@@ -1,4 +1,4 @@
1
- import { BUG_ANALYZE_PROMPT_TEMPLATE, BUG_FIX_PROMPT_TEMPLATE, IMPLEMENT_PROMPT_TEMPLATE, MR_DESCRIPTION_PROMPT_TEMPLATE, PLAN_PROMPT_TEMPLATE, REVIEW_FIX_PROMPT_TEMPLATE, REVIEW_PROMPT_TEMPLATE, REVIEW_REPLY_PROMPT_TEMPLATE, REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE, REVIEW_SUMMARY_PROMPT_TEMPLATE, RUN_LINTER_LOOP_FIX_PROMPT_TEMPLATE, RUN_TESTS_LOOP_FIX_PROMPT_TEMPLATE, TASK_SUMMARY_PROMPT_TEMPLATE, TEST_FIX_PROMPT_TEMPLATE, TEST_LINTER_FIX_PROMPT_TEMPLATE, } from "../prompts.js";
1
+ import { BUG_ANALYZE_PROMPT_TEMPLATE, BUG_FIX_PROMPT_TEMPLATE, IMPLEMENT_PROMPT_TEMPLATE, MR_DESCRIPTION_PROMPT_TEMPLATE, PLAN_PROMPT_TEMPLATE, REVIEW_FIX_PROMPT_TEMPLATE, REVIEW_PROMPT_TEMPLATE, REVIEW_REPLY_PROMPT_TEMPLATE, REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE, REVIEW_SUMMARY_PROMPT_TEMPLATE, RUN_LINTER_LOOP_FIX_PROMPT_TEMPLATE, RUN_TESTS_LOOP_FIX_PROMPT_TEMPLATE, TASK_SUMMARY_PROMPT_TEMPLATE, } from "../prompts.js";
2
2
  const promptTemplates = {
3
3
  "bug-analyze": BUG_ANALYZE_PROMPT_TEMPLATE,
4
4
  "bug-fix": BUG_FIX_PROMPT_TEMPLATE,
@@ -13,8 +13,6 @@ const promptTemplates = {
13
13
  "run-linter-loop-fix": RUN_LINTER_LOOP_FIX_PROMPT_TEMPLATE,
14
14
  "run-tests-loop-fix": RUN_TESTS_LOOP_FIX_PROMPT_TEMPLATE,
15
15
  "task-summary": TASK_SUMMARY_PROMPT_TEMPLATE,
16
- "test-fix": TEST_FIX_PROMPT_TEMPLATE,
17
- "test-linter-fix": TEST_LINTER_FIX_PROMPT_TEMPLATE,
18
16
  };
19
17
  export function isPromptTemplateRef(value) {
20
18
  return value in promptTemplates;
@@ -2,12 +2,14 @@ import { commandCheckExecutor } from "../executors/command-check-executor.js";
2
2
  import { claudeExecutor } from "../executors/claude-executor.js";
3
3
  import { codexDockerExecutor } from "../executors/codex-docker-executor.js";
4
4
  import { codexLocalExecutor } from "../executors/codex-local-executor.js";
5
+ import { fetchGitLabReviewExecutor } from "../executors/fetch-gitlab-review-executor.js";
5
6
  import { jiraFetchExecutor } from "../executors/jira-fetch-executor.js";
6
7
  import { processExecutor } from "../executors/process-executor.js";
7
8
  import { verifyBuildExecutor } from "../executors/verify-build-executor.js";
8
9
  const builtInExecutors = {
9
10
  process: processExecutor,
10
11
  "command-check": commandCheckExecutor,
12
+ "fetch-gitlab-review": fetchGitLabReviewExecutor,
11
13
  "jira-fetch": jiraFetchExecutor,
12
14
  "codex-local": codexLocalExecutor,
13
15
  "codex-docker": codexDockerExecutor,
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { artifactFile, bugAnalyzeArtifacts, bugAnalyzeFile, bugAnalyzeJsonFile, bugFixDesignFile, bugFixDesignJsonFile, bugFixPlanFile, bugFixPlanJsonFile, designFile, designJsonFile, jiraDescriptionFile, jiraDescriptionJsonFile, jiraTaskFile, mrDescriptionFile, mrDescriptionJsonFile, planArtifacts, planFile, planJsonFile, qaFile, qaJsonFile, readyToMergeFile, reviewFile, reviewFixFile, reviewFixJsonFile, reviewJsonFile, reviewReplyFile, reviewReplyJsonFile, taskSummaryFile, taskSummaryJsonFile, } from "../artifacts.js";
2
+ import { artifactFile, bugAnalyzeArtifacts, bugAnalyzeFile, bugAnalyzeJsonFile, bugFixDesignFile, bugFixDesignJsonFile, bugFixPlanFile, bugFixPlanJsonFile, designFile, designJsonFile, gitlabReviewFile, gitlabReviewInputJsonFile, gitlabReviewJsonFile, jiraDescriptionFile, jiraDescriptionJsonFile, jiraTaskFile, mrDescriptionFile, mrDescriptionJsonFile, planArtifacts, planFile, planJsonFile, qaFile, qaJsonFile, readyToMergeFile, reviewFile, reviewFixFile, reviewFixJsonFile, reviewJsonFile, reviewReplyFile, reviewReplyJsonFile, taskSummaryFile, taskSummaryJsonFile, } from "../artifacts.js";
3
3
  import { TaskRunnerError } from "../errors.js";
4
4
  import { formatTemplate } from "../prompts.js";
5
5
  function readStepRef(segments, context, originalPath) {
@@ -88,6 +88,12 @@ function resolveArtifact(spec, context) {
88
88
  return designFile(taskKey);
89
89
  case "design-json-file":
90
90
  return designJsonFile(taskKey);
91
+ case "gitlab-review-file":
92
+ return gitlabReviewFile(taskKey);
93
+ case "gitlab-review-input-json-file":
94
+ return gitlabReviewInputJsonFile(taskKey);
95
+ case "gitlab-review-json-file":
96
+ return gitlabReviewJsonFile(taskKey);
91
97
  case "jira-description-file":
92
98
  return jiraDescriptionFile(taskKey);
93
99
  case "jira-description-json-file":
package/dist/prompts.js CHANGED
@@ -56,8 +56,6 @@ export const REVIEW_FIX_PROMPT_TEMPLATE = "Используй только ст
56
56
  export const TASK_SUMMARY_PROMPT_TEMPLATE = "Посмотри в {jira_task_file}. " +
57
57
  "Сделай краткое резюме задачи, на 1-2 абзаца. " +
58
58
  "Сначала запиши source-of-truth JSON в {task_summary_json_file} в виде объекта { summary: string }, затем markdown-версию в {task_summary_file}.";
59
- export const TEST_FIX_PROMPT_TEMPLATE = "Прогони тесты, исправь ошибки.";
60
- export const TEST_LINTER_FIX_PROMPT_TEMPLATE = "Прогони линтер, исправь замечания.";
61
59
  export const RUN_TESTS_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_tests.sh, проанализируй последнюю ошибку проверки, исправь код и подготовь изменения так, чтобы следующий прогон run_tests.sh прошёл успешно.";
62
60
  export const RUN_LINTER_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_linter.sh, проанализируй последнюю ошибку линтера или генерации, исправь код и подготовь изменения так, чтобы следующий прогон run_linter.sh прошёл успешно.";
63
61
  export const AUTO_REVIEW_FIX_EXTRA_PROMPT = "Исправлять только блокеры, критикалы и важные";
@@ -54,6 +54,11 @@ function validateBriefText(value, path, issues) {
54
54
  }
55
55
  expectNonEmptyString(value.summary, `${path}.summary`, issues);
56
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
+ }
57
62
  function implementationDesignSchema() {
58
63
  return {
59
64
  id: "implementation-design/v1",
@@ -204,6 +209,33 @@ function reviewFixReportSchema() {
204
209
  },
205
210
  };
206
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;
219
+ }
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
+ }
207
239
  function userInputSchema() {
208
240
  return {
209
241
  id: "user-input/v1",
@@ -223,6 +255,7 @@ const schemas = {
223
255
  "bug-analysis/v1": bugAnalysisSchema(),
224
256
  "bug-fix-design/v1": bugFixDesignSchema(),
225
257
  "bug-fix-plan/v1": bugFixPlanSchema(),
258
+ "gitlab-review/v1": gitlabReviewSchema(),
226
259
  "implementation-design/v1": implementationDesignSchema(),
227
260
  "implementation-plan/v1": implementationPlanSchema(),
228
261
  "jira-description/v1": { id: "jira-description/v1", validate: ({ path, value }) => {