agentweaver 0.1.5 → 0.1.7

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 (56) hide show
  1. package/Dockerfile.codex +56 -0
  2. package/README.md +38 -15
  3. package/dist/artifacts.js +38 -8
  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/flow-state.js +134 -0
  7. package/dist/gitlab.js +153 -0
  8. package/dist/index.js +397 -250
  9. package/dist/interactive-ui.js +170 -42
  10. package/dist/pipeline/declarative-flow-runner.js +28 -0
  11. package/dist/pipeline/flow-specs/auto.json +530 -392
  12. package/dist/pipeline/flow-specs/bug-analyze.json +149 -0
  13. package/dist/pipeline/flow-specs/gitlab-review.json +347 -0
  14. package/dist/pipeline/flow-specs/implement.json +0 -9
  15. package/dist/pipeline/flow-specs/plan.json +133 -0
  16. package/dist/pipeline/flow-specs/review-fix.json +2 -11
  17. package/dist/pipeline/flow-specs/review-project.json +243 -0
  18. package/dist/pipeline/flow-specs/run-go-linter-loop.json +155 -0
  19. package/dist/pipeline/flow-specs/run-go-tests-loop.json +155 -0
  20. package/dist/pipeline/flow-specs/run-linter-loop.json +17 -11
  21. package/dist/pipeline/flow-specs/run-tests-loop.json +17 -11
  22. package/dist/pipeline/flow-specs/task-describe.json +25 -0
  23. package/dist/pipeline/node-registry.js +28 -1
  24. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +34 -0
  25. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +105 -0
  26. package/dist/pipeline/nodes/jira-issue-check-node.js +53 -0
  27. package/dist/pipeline/nodes/local-script-check-node.js +81 -0
  28. package/dist/pipeline/nodes/review-findings-form-node.js +14 -14
  29. package/dist/pipeline/prompt-registry.js +5 -5
  30. package/dist/pipeline/registry.js +2 -0
  31. package/dist/pipeline/value-resolver.js +7 -1
  32. package/dist/prompts.js +11 -4
  33. package/dist/scope.js +118 -0
  34. package/dist/structured-artifacts.js +33 -0
  35. package/docker-compose.yml +445 -0
  36. package/package.json +8 -3
  37. package/run_go_coverage.sh +113 -0
  38. package/run_go_linter.sh +89 -0
  39. package/run_go_tests.sh +83 -0
  40. package/verify_build.sh +105 -0
  41. package/dist/executors/claude-summary-executor.js +0 -31
  42. package/dist/executors/configs/claude-summary-config.js +0 -8
  43. package/dist/pipeline/flow-runner.js +0 -13
  44. package/dist/pipeline/flow-specs/test-fix.json +0 -24
  45. package/dist/pipeline/flow-specs/test-linter-fix.json +0 -24
  46. package/dist/pipeline/flow-specs/test.json +0 -19
  47. package/dist/pipeline/flow-types.js +0 -1
  48. package/dist/pipeline/flows/implement-flow.js +0 -47
  49. package/dist/pipeline/flows/plan-flow.js +0 -42
  50. package/dist/pipeline/flows/review-fix-flow.js +0 -62
  51. package/dist/pipeline/flows/review-flow.js +0 -124
  52. package/dist/pipeline/flows/test-fix-flow.js +0 -12
  53. package/dist/pipeline/flows/test-flow.js +0 -32
  54. package/dist/pipeline/nodes/claude-summary-node.js +0 -38
  55. package/dist/pipeline/nodes/implement-codex-node.js +0 -16
  56. package/dist/pipeline/nodes/task-summary-node.js +0 -42
@@ -5,6 +5,31 @@
5
5
  {
6
6
  "id": "task_describe",
7
7
  "steps": [
8
+ {
9
+ "id": "fetch_jira",
10
+ "node": "jira-fetch",
11
+ "params": {
12
+ "jiraApiUrl": { "ref": "params.jiraApiUrl" },
13
+ "outputFile": {
14
+ "artifact": {
15
+ "kind": "jira-task-file",
16
+ "taskKey": { "ref": "params.taskKey" }
17
+ }
18
+ }
19
+ },
20
+ "expect": [
21
+ {
22
+ "kind": "require-file",
23
+ "path": {
24
+ "artifact": {
25
+ "kind": "jira-task-file",
26
+ "taskKey": { "ref": "params.taskKey" }
27
+ }
28
+ },
29
+ "message": "Jira fetch node did not produce the Jira task file."
30
+ }
31
+ ]
32
+ },
8
33
  {
9
34
  "id": "run_codex_task_describe",
10
35
  "node": "codex-local-prompt",
@@ -3,9 +3,13 @@ import { claudePromptNode } from "./nodes/claude-prompt-node.js";
3
3
  import { codexDockerPromptNode } from "./nodes/codex-docker-prompt-node.js";
4
4
  import { codexLocalPromptNode } from "./nodes/codex-local-prompt-node.js";
5
5
  import { commandCheckNode } from "./nodes/command-check-node.js";
6
+ import { fetchGitLabReviewNode } from "./nodes/fetch-gitlab-review-node.js";
6
7
  import { fileCheckNode } from "./nodes/file-check-node.js";
7
8
  import { flowRunNode } from "./nodes/flow-run-node.js";
9
+ import { gitlabReviewArtifactsNode } from "./nodes/gitlab-review-artifacts-node.js";
8
10
  import { jiraFetchNode } from "./nodes/jira-fetch-node.js";
11
+ import { jiraIssueCheckNode } from "./nodes/jira-issue-check-node.js";
12
+ import { localScriptCheckNode } from "./nodes/local-script-check-node.js";
9
13
  import { planCodexNode } from "./nodes/plan-codex-node.js";
10
14
  import { reviewClaudeNode } from "./nodes/review-claude-node.js";
11
15
  import { reviewFindingsFormNode } from "./nodes/review-findings-form-node.js";
@@ -19,9 +23,13 @@ const builtInNodes = {
19
23
  "codex-docker-prompt": codexDockerPromptNode,
20
24
  "codex-local-prompt": codexLocalPromptNode,
21
25
  "command-check": commandCheckNode,
26
+ "fetch-gitlab-review": fetchGitLabReviewNode,
22
27
  "file-check": fileCheckNode,
23
28
  "flow-run": flowRunNode,
29
+ "gitlab-review-artifacts": gitlabReviewArtifactsNode,
24
30
  "jira-fetch": jiraFetchNode,
31
+ "jira-issue-check": jiraIssueCheckNode,
32
+ "local-script-check": localScriptCheckNode,
25
33
  "plan-codex": planCodexNode,
26
34
  "review-claude": reviewClaudeNode,
27
35
  "review-findings-form": reviewFindingsFormNode,
@@ -41,9 +49,28 @@ const builtInNodeMetadata = {
41
49
  },
42
50
  "codex-local-prompt": { kind: "codex-local-prompt", version: 1, prompt: "required", requiredParams: ["labelText"] },
43
51
  "command-check": { kind: "command-check", version: 1, prompt: "forbidden", requiredParams: ["commands"] },
52
+ "fetch-gitlab-review": {
53
+ kind: "fetch-gitlab-review",
54
+ version: 1,
55
+ prompt: "forbidden",
56
+ requiredParams: ["mergeRequestUrl", "outputFile", "outputJsonFile"],
57
+ },
44
58
  "file-check": { kind: "file-check", version: 1, prompt: "forbidden", requiredParams: ["path"] },
45
59
  "flow-run": { kind: "flow-run", version: 1, prompt: "forbidden", requiredParams: ["fileName"] },
60
+ "gitlab-review-artifacts": {
61
+ kind: "gitlab-review-artifacts",
62
+ version: 1,
63
+ prompt: "forbidden",
64
+ requiredParams: ["gitlabReviewJsonFile", "reviewFile", "reviewJsonFile"],
65
+ },
46
66
  "jira-fetch": { kind: "jira-fetch", version: 1, prompt: "forbidden", requiredParams: ["jiraApiUrl", "outputFile"] },
67
+ "jira-issue-check": {
68
+ kind: "jira-issue-check",
69
+ version: 1,
70
+ prompt: "forbidden",
71
+ requiredParams: ["jiraTaskFile", "allowedIssueTypes"],
72
+ },
73
+ "local-script-check": { kind: "local-script-check", version: 1, prompt: "forbidden", requiredParams: ["argv", "labelText"] },
47
74
  "plan-codex": { kind: "plan-codex", version: 1, prompt: "forbidden", requiredParams: ["prompt", "requiredArtifacts"] },
48
75
  "review-claude": {
49
76
  kind: "review-claude",
@@ -55,7 +82,7 @@ const builtInNodeMetadata = {
55
82
  kind: "review-findings-form",
56
83
  version: 1,
57
84
  prompt: "forbidden",
58
- requiredParams: ["reviewJsonFile", "formId", "title"],
85
+ requiredParams: ["reviewReplyJsonFile", "formId", "title"],
59
86
  },
60
87
  "review-reply-codex": {
61
88
  kind: "review-reply-codex",
@@ -0,0 +1,34 @@
1
+ import { toExecutorContext } from "../types.js";
2
+ export const fetchGitLabReviewNode = {
3
+ kind: "fetch-gitlab-review",
4
+ version: 1,
5
+ async run(context, params) {
6
+ const executor = context.executors.get("fetch-gitlab-review");
7
+ const value = await executor.execute(toExecutorContext(context), {
8
+ mergeRequestUrl: params.mergeRequestUrl,
9
+ outputFile: params.outputFile,
10
+ outputJsonFile: params.outputJsonFile,
11
+ }, executor.defaultConfig);
12
+ return {
13
+ value,
14
+ outputs: [
15
+ { kind: "artifact", path: params.outputFile, required: true },
16
+ { kind: "artifact", path: params.outputJsonFile, required: true },
17
+ ],
18
+ };
19
+ },
20
+ checks(_context, params) {
21
+ return [
22
+ {
23
+ kind: "require-file",
24
+ path: params.outputFile,
25
+ message: `Fetch GitLab review node did not produce ${params.outputFile}.`,
26
+ },
27
+ {
28
+ kind: "require-file",
29
+ path: params.outputJsonFile,
30
+ message: `Fetch GitLab review node did not produce ${params.outputJsonFile}.`,
31
+ },
32
+ ];
33
+ },
34
+ };
@@ -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,53 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { TaskRunnerError } from "../../errors.js";
3
+ import { printInfo } from "../../tui.js";
4
+ function extractIssueTypeName(path) {
5
+ let parsed;
6
+ try {
7
+ parsed = JSON.parse(readFileSync(path, "utf8"));
8
+ }
9
+ catch (error) {
10
+ throw new TaskRunnerError(`Failed to parse Jira issue JSON ${path}: ${error.message}`);
11
+ }
12
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
13
+ throw new TaskRunnerError(`Jira issue payload in ${path} must be a JSON object.`);
14
+ }
15
+ const fields = parsed.fields;
16
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
17
+ throw new TaskRunnerError(`Jira issue payload in ${path} does not contain 'fields'.`);
18
+ }
19
+ const issueType = fields.issuetype;
20
+ if (!issueType || typeof issueType !== "object" || Array.isArray(issueType)) {
21
+ throw new TaskRunnerError(`Jira issue payload in ${path} does not contain 'fields.issuetype'.`);
22
+ }
23
+ const issueTypeName = issueType.name;
24
+ if (typeof issueTypeName !== "string" || issueTypeName.trim().length === 0) {
25
+ throw new TaskRunnerError(`Jira issue payload in ${path} does not contain 'fields.issuetype.name'.`);
26
+ }
27
+ return issueTypeName.trim();
28
+ }
29
+ export const jiraIssueCheckNode = {
30
+ kind: "jira-issue-check",
31
+ version: 1,
32
+ async run(_context, params) {
33
+ if (params.labelText) {
34
+ printInfo(params.labelText);
35
+ }
36
+ const issueType = extractIssueTypeName(params.jiraTaskFile);
37
+ const normalizedIssueType = issueType.toLowerCase();
38
+ const allowedIssueTypes = params.allowedIssueTypes
39
+ .map((candidate) => candidate.trim())
40
+ .filter((candidate) => candidate.length > 0);
41
+ if (allowedIssueTypes.length === 0) {
42
+ throw new TaskRunnerError("jira-issue-check requires at least one allowed issue type.");
43
+ }
44
+ const isAllowed = allowedIssueTypes.some((candidate) => candidate.toLowerCase() === normalizedIssueType);
45
+ if (!isAllowed) {
46
+ throw new TaskRunnerError(`Flow 'bug-analyze' supports only Jira issue types: ${allowedIssueTypes.join(", ")}. ` +
47
+ `Fetched issue type: ${issueType}.`);
48
+ }
49
+ return {
50
+ value: { issueType },
51
+ };
52
+ },
53
+ };
@@ -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_PROJECT_PROMPT_TEMPLATE, REVIEW_PROMPT_TEMPLATE, REVIEW_REPLY_PROJECT_PROMPT_TEMPLATE, REVIEW_REPLY_PROMPT_TEMPLATE, REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE, REVIEW_SUMMARY_PROMPT_TEMPLATE, RUN_GO_LINTER_LOOP_FIX_PROMPT_TEMPLATE, RUN_GO_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,
@@ -6,15 +6,15 @@ const promptTemplates = {
6
6
  "mr-description": MR_DESCRIPTION_PROMPT_TEMPLATE,
7
7
  plan: PLAN_PROMPT_TEMPLATE,
8
8
  review: REVIEW_PROMPT_TEMPLATE,
9
+ "review-project": REVIEW_PROJECT_PROMPT_TEMPLATE,
9
10
  "review-fix": REVIEW_FIX_PROMPT_TEMPLATE,
10
11
  "review-reply": REVIEW_REPLY_PROMPT_TEMPLATE,
12
+ "review-reply-project": REVIEW_REPLY_PROJECT_PROMPT_TEMPLATE,
11
13
  "review-reply-summary": REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE,
12
14
  "review-summary": REVIEW_SUMMARY_PROMPT_TEMPLATE,
13
- "run-linter-loop-fix": RUN_LINTER_LOOP_FIX_PROMPT_TEMPLATE,
14
- "run-tests-loop-fix": RUN_TESTS_LOOP_FIX_PROMPT_TEMPLATE,
15
+ "run-go-linter-loop-fix": RUN_GO_LINTER_LOOP_FIX_PROMPT_TEMPLATE,
16
+ "run-go-tests-loop-fix": RUN_GO_TESTS_LOOP_FIX_PROMPT_TEMPLATE,
15
17
  "task-summary": TASK_SUMMARY_PROMPT_TEMPLATE,
16
- "test-fix": TEST_FIX_PROMPT_TEMPLATE,
17
- "test-linter-fix": TEST_LINTER_FIX_PROMPT_TEMPLATE,
18
18
  };
19
19
  export function isPromptTemplateRef(value) {
20
20
  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
@@ -37,10 +37,19 @@ export const REVIEW_PROMPT_TEMPLATE = "Проведи код-ревью теку
37
37
  "Сначала запиши структурированный результат в {review_json_file} в виде объекта { summary: string, ready_to_merge: boolean, findings: [{ severity: string, title: string, description: string }] }. " +
38
38
  "Затем запиши производную markdown-версию в {review_file}. " +
39
39
  "Если ready_to_merge=true и нет блокеров, препятствующих merge - создай файл ready-to-merge.md.";
40
+ export const REVIEW_PROJECT_PROMPT_TEMPLATE = "Проведи код-ревью текущих изменений в проекте без Jira-контекста. " +
41
+ "Оцени качество изменений по текущему коду, тестам, рискам регрессий и общему инженерному качеству. " +
42
+ "Сначала запиши структурированный результат в {review_json_file} в виде объекта { summary: string, ready_to_merge: boolean, findings: [{ severity: string, title: string, description: string }] }. " +
43
+ "Затем запиши производную markdown-версию в {review_file}. " +
44
+ "Если ready_to_merge=true и нет блокеров, создай файл {ready_to_merge_file}.";
40
45
  export const REVIEW_REPLY_PROMPT_TEMPLATE = "Твой коллега провёл код-ревью и записал структурированный результат в {review_json_file}. " +
41
46
  "Используй только структурированные артефакты как source of truth: задачу в {jira_task_file}, дизайн в {design_json_file}, план в {plan_json_file} и review в {review_json_file}. " +
42
47
  "Сначала запиши структурированный ответ в {review_reply_json_file} в виде объекта { summary: string, ready_to_merge: boolean, responses: [{ finding_title: string, disposition: string, action: string }] }. " +
43
48
  "Затем запиши производную markdown-версию в {review_reply_file}.";
49
+ export const REVIEW_REPLY_PROJECT_PROMPT_TEMPLATE = "Твой коллега провёл код-ревью и записал структурированный результат в {review_json_file}. " +
50
+ "Используй review в {review_json_file} как source of truth, разберись в замечаниях и подготовь структурированный ответ. " +
51
+ "Сначала запиши структурированный ответ в {review_reply_json_file} в виде объекта { summary: string, ready_to_merge: boolean, responses: [{ finding_title: string, disposition: string, action: string }] }. " +
52
+ "Затем запиши производную markdown-версию в {review_reply_file}.";
44
53
  export const REVIEW_SUMMARY_PROMPT_TEMPLATE = "Посмотри в {review_file}. " +
45
54
  "Сделай краткий список комментариев без подробностей, 3-7 пунктов. " +
46
55
  "Запиши результат в {review_summary_file}.";
@@ -56,10 +65,8 @@ export const REVIEW_FIX_PROMPT_TEMPLATE = "Используй только ст
56
65
  export const TASK_SUMMARY_PROMPT_TEMPLATE = "Посмотри в {jira_task_file}. " +
57
66
  "Сделай краткое резюме задачи, на 1-2 абзаца. " +
58
67
  "Сначала запиши 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
- export const RUN_TESTS_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_tests.sh, проанализируй последнюю ошибку проверки, исправь код и подготовь изменения так, чтобы следующий прогон run_tests.sh прошёл успешно.";
62
- export const RUN_LINTER_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_linter.sh, проанализируй последнюю ошибку линтера или генерации, исправь код и подготовь изменения так, чтобы следующий прогон run_linter.sh прошёл успешно.";
68
+ export const RUN_GO_TESTS_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_go_tests.sh, проанализируй последнюю ошибку проверки, исправь код и подготовь изменения так, чтобы следующий прогон run_go_tests.sh прошёл успешно.";
69
+ export const RUN_GO_LINTER_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_go_linter.sh, проанализируй последнюю ошибку линтера или генерации, исправь код и подготовь изменения так, чтобы следующий прогон run_go_linter.sh прошёл успешно.";
63
70
  export const AUTO_REVIEW_FIX_EXTRA_PROMPT = "Исправлять только блокеры, критикалы и важные";
64
71
  export function formatTemplate(template, values) {
65
72
  let result = template;
package/dist/scope.js ADDED
@@ -0,0 +1,118 @@
1
+ import crypto from "node:crypto";
2
+ import process from "node:process";
3
+ import { execFileSync } from "node:child_process";
4
+ import { ensureScopeWorkspaceDir, jiraTaskFile } from "./artifacts.js";
5
+ import { TaskRunnerError } from "./errors.js";
6
+ import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey } from "./jira.js";
7
+ function gitOutput(args) {
8
+ try {
9
+ const output = execFileSync("git", args, {
10
+ cwd: process.cwd(),
11
+ encoding: "utf8",
12
+ stdio: ["ignore", "pipe", "ignore"],
13
+ });
14
+ const trimmed = output.trim();
15
+ return trimmed.length > 0 ? trimmed : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ function shortHash(input) {
22
+ return crypto.createHash("sha1").update(input).digest("hex").slice(0, 8);
23
+ }
24
+ export function sanitizeScopeName(value) {
25
+ const normalized = value
26
+ .trim()
27
+ .toLowerCase()
28
+ .replaceAll(/[^a-z0-9._@-]+/g, "-")
29
+ .replaceAll(/-+/g, "-")
30
+ .replaceAll(/^-|-$/g, "");
31
+ if (!normalized) {
32
+ throw new TaskRunnerError("Scope name is empty after sanitization. Use letters, digits, '.', '_', '-' or '@'.");
33
+ }
34
+ return normalized;
35
+ }
36
+ export function detectGitBranchName() {
37
+ const branchName = gitOutput(["rev-parse", "--abbrev-ref", "HEAD"]);
38
+ if (!branchName) {
39
+ return null;
40
+ }
41
+ if (branchName === "HEAD") {
42
+ return null;
43
+ }
44
+ return branchName;
45
+ }
46
+ export function detectProjectRoot() {
47
+ return gitOutput(["rev-parse", "--show-toplevel"]) ?? process.cwd();
48
+ }
49
+ export function buildProjectScopeKey(explicitScope) {
50
+ const projectRoot = detectProjectRoot();
51
+ const worktreeHash = shortHash(projectRoot);
52
+ if (explicitScope?.trim()) {
53
+ return {
54
+ scopeKey: sanitizeScopeName(explicitScope),
55
+ gitBranchName: detectGitBranchName(),
56
+ worktreeHash,
57
+ projectRoot,
58
+ };
59
+ }
60
+ const branchName = detectGitBranchName();
61
+ const branchSlug = sanitizeScopeName(branchName ?? "detached-head");
62
+ return {
63
+ scopeKey: `${branchSlug}@${worktreeHash}`,
64
+ gitBranchName: branchName,
65
+ worktreeHash,
66
+ projectRoot,
67
+ };
68
+ }
69
+ export function resolveTaskScope(jiraRef, explicitScope) {
70
+ const jiraIssueKey = extractIssueKey(jiraRef);
71
+ const scopeKey = explicitScope?.trim() ? sanitizeScopeName(explicitScope) : jiraIssueKey;
72
+ ensureScopeWorkspaceDir(scopeKey);
73
+ return {
74
+ scopeType: "task",
75
+ scopeKey,
76
+ jiraRef,
77
+ jiraIssueKey,
78
+ jiraBrowseUrl: buildJiraBrowseUrl(jiraRef),
79
+ jiraApiUrl: buildJiraApiUrl(jiraRef),
80
+ jiraTaskFile: jiraTaskFile(scopeKey),
81
+ };
82
+ }
83
+ export function resolveProjectScope(explicitScope) {
84
+ const { scopeKey, gitBranchName, worktreeHash, projectRoot } = buildProjectScopeKey(explicitScope);
85
+ ensureScopeWorkspaceDir(scopeKey);
86
+ return {
87
+ scopeType: "project",
88
+ scopeKey,
89
+ gitBranchName,
90
+ worktreeHash,
91
+ projectRoot,
92
+ };
93
+ }
94
+ export function buildJiraTaskInputForm() {
95
+ return {
96
+ formId: "jira-task-input",
97
+ title: "Jira Task",
98
+ description: "Укажи Jira issue key или browse URL для task-driven flow.",
99
+ submitLabel: "Continue",
100
+ fields: [
101
+ {
102
+ id: "jira_ref",
103
+ type: "text",
104
+ label: "Jira issue key or browse URL",
105
+ help: "Например: DEMO-3288 или https://jira.example.ru/browse/DEMO-3288",
106
+ required: true,
107
+ },
108
+ ],
109
+ };
110
+ }
111
+ export async function requestTaskScope(requestUserInput) {
112
+ const result = await requestUserInput(buildJiraTaskInputForm());
113
+ const jiraRef = String(result.values.jira_ref ?? "").trim();
114
+ if (!jiraRef) {
115
+ throw new TaskRunnerError("Jira issue key or browse URL is required.");
116
+ }
117
+ return resolveTaskScope(jiraRef);
118
+ }