agentweaver 0.1.6 → 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.
- package/Dockerfile.codex +4 -3
- package/README.md +31 -12
- package/dist/artifacts.js +29 -8
- package/dist/flow-state.js +134 -0
- package/dist/index.js +368 -171
- package/dist/interactive-ui.js +170 -42
- package/dist/pipeline/declarative-flow-runner.js +28 -0
- package/dist/pipeline/flow-specs/auto.json +530 -392
- package/dist/pipeline/flow-specs/bug-analyze.json +149 -0
- package/dist/pipeline/flow-specs/plan.json +133 -0
- package/dist/pipeline/flow-specs/review-project.json +243 -0
- package/dist/pipeline/flow-specs/run-go-linter-loop.json +155 -0
- package/dist/pipeline/flow-specs/run-go-tests-loop.json +155 -0
- package/dist/pipeline/flow-specs/task-describe.json +25 -0
- package/dist/pipeline/node-registry.js +8 -0
- package/dist/pipeline/nodes/jira-issue-check-node.js +53 -0
- package/dist/pipeline/prompt-registry.js +5 -3
- package/dist/prompts.js +11 -2
- package/dist/scope.js +118 -0
- package/docker-compose.yml +65 -4
- package/package.json +4 -3
- package/{run_tests.sh → run_go_coverage.sh} +4 -4
- package/{run_linter.sh → run_go_linter.sh} +2 -2
- package/run_go_tests.sh +83 -0
- package/verify_build.sh +4 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "run-go-tests-loop-flow",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"phases": [
|
|
5
|
+
{
|
|
6
|
+
"id": "run_go_tests_try_1",
|
|
7
|
+
"steps": [
|
|
8
|
+
{
|
|
9
|
+
"id": "run_go_tests",
|
|
10
|
+
"node": "local-script-check",
|
|
11
|
+
"params": {
|
|
12
|
+
"argv": {
|
|
13
|
+
"list": [
|
|
14
|
+
{ "ref": "params.runGoTestsScript" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"labelText": {
|
|
18
|
+
"const": "Running run_go_tests.sh locally (attempt 1)"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"stopFlowIf": {
|
|
22
|
+
"equals": [
|
|
23
|
+
{ "ref": "steps.run_go_tests_try_1.run_go_tests.outputs.parsed.ok" },
|
|
24
|
+
{ "const": true }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "fix_go_tests",
|
|
30
|
+
"when": {
|
|
31
|
+
"equals": [
|
|
32
|
+
{ "ref": "steps.run_go_tests_try_1.run_go_tests.outputs.parsed.ok" },
|
|
33
|
+
{ "const": false }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"node": "codex-local-prompt",
|
|
37
|
+
"prompt": {
|
|
38
|
+
"templateRef": "run-go-tests-loop-fix",
|
|
39
|
+
"extraPrompt": {
|
|
40
|
+
"appendPrompt": {
|
|
41
|
+
"base": { "ref": "params.extraPrompt" },
|
|
42
|
+
"suffix": {
|
|
43
|
+
"template": "Последний результат run_go_tests.sh: exitCode={exit_code}, summary={summary}. Исправь только то, что мешает успешному прохождению проверки.",
|
|
44
|
+
"vars": {
|
|
45
|
+
"exit_code": { "ref": "steps.run_go_tests_try_1.run_go_tests.outputs.parsed.exitCode" },
|
|
46
|
+
"summary": { "ref": "steps.run_go_tests_try_1.run_go_tests.outputs.parsed.summary" }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"format": "task-prompt"
|
|
52
|
+
},
|
|
53
|
+
"params": {
|
|
54
|
+
"labelText": {
|
|
55
|
+
"const": "Running Codex Go tests loop fix (attempt 1)"
|
|
56
|
+
},
|
|
57
|
+
"model": { "const": "gpt-5.4" }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"repeat": {
|
|
64
|
+
"var": "attempt",
|
|
65
|
+
"from": 2,
|
|
66
|
+
"to": 5
|
|
67
|
+
},
|
|
68
|
+
"phases": [
|
|
69
|
+
{
|
|
70
|
+
"id": "run_go_tests_try_${attempt}",
|
|
71
|
+
"when": {
|
|
72
|
+
"equals": [
|
|
73
|
+
{ "ref": "steps.run_go_tests_try_${attempt_minus_one}.run_go_tests.outputs.parsed.ok" },
|
|
74
|
+
{ "const": false }
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"steps": [
|
|
78
|
+
{
|
|
79
|
+
"id": "run_go_tests",
|
|
80
|
+
"node": "local-script-check",
|
|
81
|
+
"params": {
|
|
82
|
+
"argv": {
|
|
83
|
+
"list": [
|
|
84
|
+
{ "ref": "params.runGoTestsScript" }
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"labelText": {
|
|
88
|
+
"template": "Running run_go_tests.sh locally (attempt ${attempt})"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"stopFlowIf": {
|
|
92
|
+
"equals": [
|
|
93
|
+
{ "ref": "steps.run_go_tests_try_${attempt}.run_go_tests.outputs.parsed.ok" },
|
|
94
|
+
{ "const": true }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "fix_go_tests",
|
|
100
|
+
"when": {
|
|
101
|
+
"equals": [
|
|
102
|
+
{ "ref": "steps.run_go_tests_try_${attempt}.run_go_tests.outputs.parsed.ok" },
|
|
103
|
+
{ "const": false }
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
"node": "codex-local-prompt",
|
|
107
|
+
"prompt": {
|
|
108
|
+
"templateRef": "run-go-tests-loop-fix",
|
|
109
|
+
"extraPrompt": {
|
|
110
|
+
"appendPrompt": {
|
|
111
|
+
"base": { "ref": "params.extraPrompt" },
|
|
112
|
+
"suffix": {
|
|
113
|
+
"template": "Последний результат run_go_tests.sh: exitCode={exit_code}, summary={summary}. Исправь только то, что мешает успешному прохождению проверки.",
|
|
114
|
+
"vars": {
|
|
115
|
+
"exit_code": { "ref": "steps.run_go_tests_try_${attempt}.run_go_tests.outputs.parsed.exitCode" },
|
|
116
|
+
"summary": { "ref": "steps.run_go_tests_try_${attempt}.run_go_tests.outputs.parsed.summary" }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"format": "task-prompt"
|
|
122
|
+
},
|
|
123
|
+
"params": {
|
|
124
|
+
"labelText": {
|
|
125
|
+
"template": "Running Codex Go tests loop fix (attempt ${attempt})"
|
|
126
|
+
},
|
|
127
|
+
"model": { "const": "gpt-5.4" }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "run_go_tests_failed",
|
|
136
|
+
"steps": [
|
|
137
|
+
{
|
|
138
|
+
"id": "assert_run_go_tests_success",
|
|
139
|
+
"node": "file-check",
|
|
140
|
+
"params": {
|
|
141
|
+
"path": { "ref": "params.runGoTestsScript" }
|
|
142
|
+
},
|
|
143
|
+
"expect": [
|
|
144
|
+
{
|
|
145
|
+
"kind": "step-output",
|
|
146
|
+
"value": { "ref": "steps.run_go_tests_try_5.run_go_tests.outputs.parsed.ok" },
|
|
147
|
+
"equals": { "const": true },
|
|
148
|
+
"message": "run-go-tests-loop exhausted all attempts without a successful run_go_tests.sh execution."
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
@@ -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",
|
|
@@ -8,6 +8,7 @@ import { fileCheckNode } from "./nodes/file-check-node.js";
|
|
|
8
8
|
import { flowRunNode } from "./nodes/flow-run-node.js";
|
|
9
9
|
import { gitlabReviewArtifactsNode } from "./nodes/gitlab-review-artifacts-node.js";
|
|
10
10
|
import { jiraFetchNode } from "./nodes/jira-fetch-node.js";
|
|
11
|
+
import { jiraIssueCheckNode } from "./nodes/jira-issue-check-node.js";
|
|
11
12
|
import { localScriptCheckNode } from "./nodes/local-script-check-node.js";
|
|
12
13
|
import { planCodexNode } from "./nodes/plan-codex-node.js";
|
|
13
14
|
import { reviewClaudeNode } from "./nodes/review-claude-node.js";
|
|
@@ -27,6 +28,7 @@ const builtInNodes = {
|
|
|
27
28
|
"flow-run": flowRunNode,
|
|
28
29
|
"gitlab-review-artifacts": gitlabReviewArtifactsNode,
|
|
29
30
|
"jira-fetch": jiraFetchNode,
|
|
31
|
+
"jira-issue-check": jiraIssueCheckNode,
|
|
30
32
|
"local-script-check": localScriptCheckNode,
|
|
31
33
|
"plan-codex": planCodexNode,
|
|
32
34
|
"review-claude": reviewClaudeNode,
|
|
@@ -62,6 +64,12 @@ const builtInNodeMetadata = {
|
|
|
62
64
|
requiredParams: ["gitlabReviewJsonFile", "reviewFile", "reviewJsonFile"],
|
|
63
65
|
},
|
|
64
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
|
+
},
|
|
65
73
|
"local-script-check": { kind: "local-script-check", version: 1, prompt: "forbidden", requiredParams: ["argv", "labelText"] },
|
|
66
74
|
"plan-codex": { kind: "plan-codex", version: 1, prompt: "forbidden", requiredParams: ["prompt", "requiredArtifacts"] },
|
|
67
75
|
"review-claude": {
|
|
@@ -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
|
+
};
|
|
@@ -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,
|
|
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,12 +6,14 @@ 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":
|
|
14
|
-
"run-tests-loop-fix":
|
|
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
18
|
};
|
|
17
19
|
export function isPromptTemplateRef(value) {
|
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,8 +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
|
|
60
|
-
export const
|
|
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 прошёл успешно.";
|
|
61
70
|
export const AUTO_REVIEW_FIX_EXTRA_PROMPT = "Исправлять только блокеры, критикалы и важные";
|
|
62
71
|
export function formatTemplate(template, values) {
|
|
63
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
|
+
}
|
package/docker-compose.yml
CHANGED
|
@@ -209,7 +209,7 @@ services:
|
|
|
209
209
|
- /tmp:exec,mode=1777
|
|
210
210
|
- /root
|
|
211
211
|
|
|
212
|
-
run-tests:
|
|
212
|
+
run-go-tests:
|
|
213
213
|
build:
|
|
214
214
|
context: .
|
|
215
215
|
dockerfile: Dockerfile.codex
|
|
@@ -260,7 +260,7 @@ services:
|
|
|
260
260
|
read_only: true
|
|
261
261
|
bind:
|
|
262
262
|
create_host_path: false
|
|
263
|
-
entrypoint: ["/usr/local/bin/
|
|
263
|
+
entrypoint: ["/usr/local/bin/run_go_tests.sh"]
|
|
264
264
|
cap_drop:
|
|
265
265
|
- ALL
|
|
266
266
|
security_opt:
|
|
@@ -270,7 +270,7 @@ services:
|
|
|
270
270
|
- /tmp:exec,mode=1777
|
|
271
271
|
- /root
|
|
272
272
|
|
|
273
|
-
run-linter:
|
|
273
|
+
run-go-linter:
|
|
274
274
|
build:
|
|
275
275
|
context: .
|
|
276
276
|
dockerfile: Dockerfile.codex
|
|
@@ -321,7 +321,68 @@ services:
|
|
|
321
321
|
read_only: true
|
|
322
322
|
bind:
|
|
323
323
|
create_host_path: false
|
|
324
|
-
entrypoint: ["/usr/local/bin/
|
|
324
|
+
entrypoint: ["/usr/local/bin/run_go_linter.sh"]
|
|
325
|
+
cap_drop:
|
|
326
|
+
- ALL
|
|
327
|
+
security_opt:
|
|
328
|
+
- no-new-privileges:true
|
|
329
|
+
read_only: true
|
|
330
|
+
tmpfs:
|
|
331
|
+
- /tmp:exec,mode=1777
|
|
332
|
+
- /root
|
|
333
|
+
|
|
334
|
+
run-go-coverage:
|
|
335
|
+
build:
|
|
336
|
+
context: .
|
|
337
|
+
dockerfile: Dockerfile.codex
|
|
338
|
+
working_dir: /workspace
|
|
339
|
+
init: true
|
|
340
|
+
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
|
|
341
|
+
environment:
|
|
342
|
+
PATH: /usr/local/go/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/codex-home/go/bin:/go/bin
|
|
343
|
+
CODEX_HOME: /codex-home
|
|
344
|
+
HOME: /codex-home/home
|
|
345
|
+
XDG_CACHE_HOME: /codex-home/cache/xdg
|
|
346
|
+
GOPATH: /codex-home/go
|
|
347
|
+
GOBIN: /codex-home/go/bin
|
|
348
|
+
GOCACHE: /codex-home/cache/go-build
|
|
349
|
+
GOMODCACHE: /codex-home/cache/gomod
|
|
350
|
+
GOLANGCI_LINT_CACHE: /codex-home/cache/golangci-lint
|
|
351
|
+
GOPRIVATE: ${GOPRIVATE:-gitlab.yourdomain.org/*}
|
|
352
|
+
GONOSUMDB: ${GONOSUMDB:-gitlab.yourdomain.org/*}
|
|
353
|
+
GONOPROXY: ${GONOPROXY:-gitlab.yourdomain.org/*}
|
|
354
|
+
GIT_ALLOW_PROTOCOL: ${GIT_ALLOW_PROTOCOL:-file:https:ssh}
|
|
355
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
356
|
+
DOCKER_HOST: tcp://dockerd:2375
|
|
357
|
+
TESTCONTAINERS_HOST_OVERRIDE: dockerd
|
|
358
|
+
VERIFY_BUILD_ROOT_DIR: /workspace
|
|
359
|
+
depends_on:
|
|
360
|
+
dockerd:
|
|
361
|
+
condition: service_healthy
|
|
362
|
+
volumes:
|
|
363
|
+
- type: bind
|
|
364
|
+
source: ${PROJECT_DIR:-./}
|
|
365
|
+
target: /workspace
|
|
366
|
+
bind:
|
|
367
|
+
create_host_path: false
|
|
368
|
+
- type: bind
|
|
369
|
+
source: ${CODEX_HOME_DIR:-~/.codex}
|
|
370
|
+
target: /codex-home
|
|
371
|
+
bind:
|
|
372
|
+
create_host_path: false
|
|
373
|
+
- type: bind
|
|
374
|
+
source: ${HOST_SSH_DIR:-~/.ssh}
|
|
375
|
+
target: /codex-home/home/.ssh
|
|
376
|
+
read_only: true
|
|
377
|
+
bind:
|
|
378
|
+
create_host_path: false
|
|
379
|
+
- type: bind
|
|
380
|
+
source: ${HOST_GITCONFIG:-~/.gitconfig}
|
|
381
|
+
target: /codex-home/home/.gitconfig
|
|
382
|
+
read_only: true
|
|
383
|
+
bind:
|
|
384
|
+
create_host_path: false
|
|
385
|
+
entrypoint: ["/usr/local/bin/run_go_coverage.sh"]
|
|
325
386
|
cap_drop:
|
|
326
387
|
- ALL
|
|
327
388
|
security_opt:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentweaver",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "CLI orchestrator for Jira/Codex/Claude engineering workflows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
"docker-compose.yml",
|
|
33
33
|
"Dockerfile.codex",
|
|
34
34
|
"verify_build.sh",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
35
|
+
"run_go_tests.sh",
|
|
36
|
+
"run_go_linter.sh",
|
|
37
|
+
"run_go_coverage.sh"
|
|
37
38
|
],
|
|
38
39
|
"publishConfig": {
|
|
39
40
|
"access": "public"
|
|
@@ -69,7 +69,7 @@ fail() {
|
|
|
69
69
|
local command="$3"
|
|
70
70
|
local details_json="${4:-{}}"
|
|
71
71
|
|
|
72
|
-
emit_result false "
|
|
72
|
+
emit_result false "coverage" "run_go_coverage" "$exit_code" "$summary" "$command" "$details_json"
|
|
73
73
|
exit "$exit_code"
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -92,9 +92,9 @@ if [[ -z "$PKGS" ]]; then
|
|
|
92
92
|
fail 2 "Coverage package list is empty" "go list ./..." '{"failedStep":"go-list","reason":"no-test-packages"}'
|
|
93
93
|
fi
|
|
94
94
|
|
|
95
|
-
log "==> Running
|
|
95
|
+
log "==> Running coverage check (go test -coverprofile)"
|
|
96
96
|
if ! go test -coverpkg="$PKGS" -coverprofile="$COVER_FILE" -count=1 ./... >&2; then
|
|
97
|
-
fail 1 "go test failed" "go test -coverpkg=<pkgs> -coverprofile=<file> -count=1 ./..." "$(details_json --arg failedStep "go-test" --arg coverFile "$COVER_FILE" '{failedStep: $failedStep, coverFile: $coverFile}')"
|
|
97
|
+
fail 1 "go test for coverage failed" "go test -coverpkg=<pkgs> -coverprofile=<file> -count=1 ./..." "$(details_json --arg failedStep "go-test" --arg coverFile "$COVER_FILE" '{failedStep: $failedStep, coverFile: $coverFile}')"
|
|
98
98
|
fi
|
|
99
99
|
|
|
100
100
|
log "==> Calculating coverage summary"
|
|
@@ -110,4 +110,4 @@ if ! awk -v c="$coverage" -v min="$MIN_COVERAGE" 'BEGIN {exit (c >= min ? 0 : 1)
|
|
|
110
110
|
fail 3 "Coverage ${coverage}% is below required ${MIN_COVERAGE}%" "go test -coverprofile && go tool cover -func" "$(details_json --argjson coverage "$coverage" --argjson minCoverage "$MIN_COVERAGE" --arg failedStep "coverage-threshold" --arg coverFile "$COVER_FILE" '{coverage: $coverage, minCoverage: $minCoverage, failedStep: $failedStep, coverFile: $coverFile}')"
|
|
111
111
|
fi
|
|
112
112
|
|
|
113
|
-
emit_result true "
|
|
113
|
+
emit_result true "coverage" "run_go_coverage" 0 "Coverage passed" "go test -coverpkg=<pkgs> -coverprofile=<file> -count=1 ./..." "$(details_json --argjson coverage "$coverage" --argjson minCoverage "$MIN_COVERAGE" --arg coverFile "$COVER_FILE" '{coverage: $coverage, minCoverage: $minCoverage, coverFile: $coverFile}')"
|
|
@@ -62,7 +62,7 @@ fail() {
|
|
|
62
62
|
local command="$3"
|
|
63
63
|
local details_json="${4:-{}}"
|
|
64
64
|
|
|
65
|
-
emit_result false "linter" "
|
|
65
|
+
emit_result false "linter" "run_go_linter" "$exit_code" "$summary" "$command" "$details_json"
|
|
66
66
|
exit "$exit_code"
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -86,4 +86,4 @@ if ! golangci-lint run >&2; then
|
|
|
86
86
|
fail 1 "golangci-lint failed" "golangci-lint run" '{"failedStep":"golangci-lint"}'
|
|
87
87
|
fi
|
|
88
88
|
|
|
89
|
-
emit_result true "linter" "
|
|
89
|
+
emit_result true "linter" "run_go_linter" 0 "Linter checks passed" "go generate ./... && golangci-lint run" "$(details_json '{steps:["go-generate","golangci-lint"]}')"
|