agentweaver 0.1.7 → 0.1.9

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 (50) hide show
  1. package/Dockerfile.codex +3 -3
  2. package/README.md +24 -10
  3. package/dist/artifacts.js +30 -0
  4. package/dist/executors/configs/fetch-gitlab-diff-config.js +3 -0
  5. package/dist/executors/configs/opencode-config.js +6 -0
  6. package/dist/executors/fetch-gitlab-diff-executor.js +26 -0
  7. package/dist/executors/jira-fetch-executor.js +8 -2
  8. package/dist/executors/opencode-executor.js +35 -0
  9. package/dist/gitlab.js +199 -5
  10. package/dist/index.js +160 -121
  11. package/dist/interactive-ui.js +45 -10
  12. package/dist/jira.js +116 -14
  13. package/dist/pipeline/auto-flow.js +1 -1
  14. package/dist/pipeline/declarative-flows.js +41 -6
  15. package/dist/pipeline/flow-catalog.js +66 -0
  16. package/dist/pipeline/flow-specs/auto.json +183 -1
  17. package/dist/pipeline/flow-specs/bug-analyze.json +1 -1
  18. package/dist/pipeline/flow-specs/gitlab-diff-review.json +226 -0
  19. package/dist/pipeline/flow-specs/gitlab-review.json +1 -31
  20. package/dist/pipeline/flow-specs/plan-opencode.json +603 -0
  21. package/dist/pipeline/flow-specs/plan.json +183 -1
  22. package/dist/pipeline/flow-specs/run-go-linter-loop.json +83 -7
  23. package/dist/pipeline/flow-specs/run-go-tests-loop.json +83 -7
  24. package/dist/pipeline/flow-specs/task-describe.json +1 -1
  25. package/dist/pipeline/node-registry.js +80 -8
  26. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +34 -0
  27. package/dist/pipeline/nodes/flow-run-node.js +2 -2
  28. package/dist/pipeline/nodes/jira-fetch-node.js +26 -2
  29. package/dist/pipeline/nodes/local-script-check-node.js +50 -8
  30. package/dist/pipeline/nodes/opencode-prompt-node.js +32 -0
  31. package/dist/pipeline/nodes/planning-questions-form-node.js +69 -0
  32. package/dist/pipeline/nodes/user-input-node.js +9 -1
  33. package/dist/pipeline/prompt-registry.js +4 -1
  34. package/dist/pipeline/registry.js +10 -0
  35. package/dist/pipeline/spec-loader.js +37 -3
  36. package/dist/pipeline/spec-types.js +43 -1
  37. package/dist/pipeline/spec-validator.js +53 -7
  38. package/dist/pipeline/value-resolver.js +25 -1
  39. package/dist/prompts.js +54 -14
  40. package/dist/scope.js +25 -16
  41. package/dist/structured-artifact-schemas.json +560 -0
  42. package/dist/structured-artifacts.js +103 -262
  43. package/dist/user-input.js +7 -0
  44. package/docker-compose.yml +2 -2
  45. package/package.json +3 -3
  46. package/run_go_linter.py +128 -0
  47. package/run_go_tests.py +120 -0
  48. package/verify_build.sh +3 -3
  49. package/run_go_linter.sh +0 -89
  50. package/run_go_tests.sh +0 -83
@@ -1,284 +1,121 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { TaskRunnerError } from "./errors.js";
5
+ export const STRUCTURED_ARTIFACT_SCHEMA_IDS = [
6
+ "bug-analysis/v1",
7
+ "bug-fix-design/v1",
8
+ "bug-fix-plan/v1",
9
+ "gitlab-mr-diff/v1",
10
+ "gitlab-review/v1",
11
+ "implementation-design/v1",
12
+ "implementation-plan/v1",
13
+ "jira-description/v1",
14
+ "mr-description/v1",
15
+ "planning-questions/v1",
16
+ "qa-plan/v1",
17
+ "review-findings/v1",
18
+ "review-fix-report/v1",
19
+ "review-reply/v1",
20
+ "task-summary/v1",
21
+ "user-input/v1",
22
+ ];
23
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
24
+ const SCHEMA_REGISTRY_PATH = path.join(MODULE_DIR, "structured-artifact-schemas.json");
3
25
  function isRecord(value) {
4
26
  return typeof value === "object" && value !== null && !Array.isArray(value);
5
27
  }
6
- function expectNonEmptyString(value, path, issues) {
7
- if (typeof value !== "string" || value.trim().length === 0) {
8
- issues.push(`${path} must be a non-empty string`);
28
+ function schemaLabel(node) {
29
+ if ("anyOf" in node) {
30
+ return "a valid value";
9
31
  }
10
- }
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) {
24
- if (!Array.isArray(value)) {
25
- issues.push(`${path} must be an array`);
26
- return;
27
- }
28
- if (!allowEmpty && value.length === 0) {
29
- issues.push(`${path} must not be empty`);
30
- return;
31
- }
32
- value.forEach((item, index) => expectNonEmptyString(item, `${path}[${index}]`, issues));
33
- }
34
- function expectObjectArray(value, path, issues, validateItem, allowEmpty = false) {
35
- if (!Array.isArray(value)) {
36
- issues.push(`${path} must be an array`);
37
- return;
32
+ switch (node.type) {
33
+ case "string":
34
+ return node.nonEmpty ? "a non-empty string" : "a string";
35
+ case "boolean":
36
+ return "a boolean";
37
+ case "number":
38
+ return "a number";
39
+ case "null":
40
+ return "null";
41
+ case "array":
42
+ return "an array";
43
+ case "object":
44
+ return "an object";
38
45
  }
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
46
  }
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;
47
+ function validateNode(value, schema, currentPath) {
48
+ if ("anyOf" in schema) {
49
+ let bestIssues = null;
50
+ for (const option of schema.anyOf) {
51
+ const issues = validateNode(value, option, currentPath);
52
+ if (issues.length === 0) {
53
+ return [];
69
54
  }
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;
55
+ if (bestIssues === null || issues.length < bestIssues.length) {
56
+ bestIssues = issues;
113
57
  }
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
- };
124
- }
125
- function bugAnalysisSchema() {
126
- return {
127
- id: "bug-analysis/v1",
128
- validate({ path, value }) {
129
- const issues = [];
130
- if (!expectObject(value, path, issues)) {
131
- return issues;
132
- }
133
- expectNonEmptyString(value.summary, `${path}.summary`, issues);
134
- if (expectObject(value.suspected_root_cause, `${path}.suspected_root_cause`, issues)) {
135
- expectNonEmptyString(value.suspected_root_cause.hypothesis, `${path}.suspected_root_cause.hypothesis`, issues);
136
- expectNonEmptyString(value.suspected_root_cause.confidence, `${path}.suspected_root_cause.confidence`, issues);
58
+ }
59
+ return bestIssues ?? [`${currentPath} must be ${schemaLabel(schema)}`];
60
+ }
61
+ switch (schema.type) {
62
+ case "string":
63
+ if (typeof value !== "string" || (schema.nonEmpty && value.trim().length === 0)) {
64
+ return [`${currentPath} must be ${schemaLabel(schema)}`];
137
65
  }
138
- expectStringArray(value.reproduction_steps, `${path}.reproduction_steps`, issues);
139
- expectStringArray(value.affected_components, `${path}.affected_components`, issues);
140
- expectStringArray(value.evidence, `${path}.evidence`, issues);
141
- expectStringArray(value.risks, `${path}.risks`, issues, true);
142
- expectStringArray(value.open_questions, `${path}.open_questions`, issues, true);
143
- return issues;
144
- },
145
- };
146
- }
147
- function bugFixDesignSchema() {
148
- return {
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",
162
- validate({ path, value }) {
163
- const issues = [];
164
- if (!expectObject(value, path, issues)) {
165
- return issues;
66
+ return [];
67
+ case "boolean":
68
+ return typeof value === "boolean" ? [] : [`${currentPath} must be a boolean`];
69
+ case "number":
70
+ return typeof value === "number" && !Number.isNaN(value) ? [] : [`${currentPath} must be a number`];
71
+ case "null":
72
+ return value === null ? [] : [`${currentPath} must be null`];
73
+ case "array": {
74
+ if (!Array.isArray(value)) {
75
+ return [`${currentPath} must be an array`];
166
76
  }
167
- expectNonEmptyString(value.summary, `${path}.summary`, 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);
174
- return issues;
175
- },
176
- };
177
- }
178
- function reviewReplySchema() {
179
- return {
180
- id: "review-reply/v1",
181
- validate({ path, value }) {
182
- const issues = [];
183
- if (!expectObject(value, path, issues)) {
184
- return issues;
77
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
78
+ return [schema.minItems === 1 ? `${currentPath} must not be empty` : `${currentPath} must contain at least ${schema.minItems} items`];
185
79
  }
186
- expectNonEmptyString(value.summary, `${path}.summary`, issues);
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;
80
+ return value.flatMap((item, index) => validateNode(item, schema.items, `${currentPath}[${index}]`));
81
+ }
82
+ case "object": {
83
+ if (!isRecord(value)) {
84
+ return [`${currentPath} must be an object`];
204
85
  }
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
86
  const issues = [];
217
- if (!expectObject(value, path, issues)) {
218
- return issues;
87
+ const properties = schema.properties ?? {};
88
+ const required = new Set(schema.required ?? []);
89
+ for (const propertyName of required) {
90
+ issues.push(...validateNode(value[propertyName], properties[propertyName] ?? { type: "object" }, `${currentPath}.${propertyName}`));
219
91
  }
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);
92
+ for (const [propertyName, propertySchema] of Object.entries(properties)) {
93
+ if (required.has(propertyName) || !(propertyName in value)) {
94
+ continue;
233
95
  }
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;
96
+ issues.push(...validateNode(value[propertyName], propertySchema, `${currentPath}.${propertyName}`));
246
97
  }
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);
250
98
  return issues;
251
- },
252
- };
99
+ }
100
+ }
253
101
  }
254
- const schemas = {
255
- "bug-analysis/v1": bugAnalysisSchema(),
256
- "bug-fix-design/v1": bugFixDesignSchema(),
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(),
281
- };
102
+ function loadSchemaRegistry() {
103
+ if (!existsSync(SCHEMA_REGISTRY_PATH)) {
104
+ throw new TaskRunnerError(`Structured artifact schema registry not found: ${SCHEMA_REGISTRY_PATH}`);
105
+ }
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(readFileSync(SCHEMA_REGISTRY_PATH, "utf8"));
109
+ }
110
+ catch (error) {
111
+ throw new TaskRunnerError(`Failed to parse structured artifact schema registry: ${error.message}`);
112
+ }
113
+ if (!isRecord(parsed)) {
114
+ throw new TaskRunnerError(`Structured artifact schema registry ${SCHEMA_REGISTRY_PATH} must be a JSON object.`);
115
+ }
116
+ return parsed;
117
+ }
118
+ const schemas = loadSchemaRegistry();
282
119
  export function validateStructuredArtifact(path, schemaId) {
283
120
  if (!existsSync(path)) {
284
121
  throw new TaskRunnerError(`Structured artifact file not found: ${path}`);
@@ -290,7 +127,11 @@ export function validateStructuredArtifact(path, schemaId) {
290
127
  catch (error) {
291
128
  throw new TaskRunnerError(`Structured artifact ${path} is not valid JSON: ${error.message}`);
292
129
  }
293
- const issues = schemas[schemaId].validate({ path, value: parsed });
130
+ const schema = schemas[schemaId];
131
+ if (!schema) {
132
+ throw new TaskRunnerError(`Structured artifact schema is not registered: ${schemaId}`);
133
+ }
134
+ const issues = validateNode(parsed, schema, path);
294
135
  if (issues.length > 0) {
295
136
  throw new TaskRunnerError(`Structured artifact ${path} failed schema ${schemaId} validation:\n${issues.join("\n")}`);
296
137
  }
@@ -83,6 +83,13 @@ function parseBoolean(value) {
83
83
  return null;
84
84
  }
85
85
  export async function requestUserInputInTerminal(form) {
86
+ if (form.fields.length === 0) {
87
+ return {
88
+ formId: form.formId,
89
+ submittedAt: new Date().toISOString(),
90
+ values: {},
91
+ };
92
+ }
86
93
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
87
94
  throw new TaskRunnerError(`Flow requires interactive user input for form '${form.formId}', but no TTY is available.`);
88
95
  }
@@ -260,7 +260,7 @@ services:
260
260
  read_only: true
261
261
  bind:
262
262
  create_host_path: false
263
- entrypoint: ["/usr/local/bin/run_go_tests.sh"]
263
+ entrypoint: ["/usr/local/bin/run_go_tests.py"]
264
264
  cap_drop:
265
265
  - ALL
266
266
  security_opt:
@@ -321,7 +321,7 @@ services:
321
321
  read_only: true
322
322
  bind:
323
323
  create_host_path: false
324
- entrypoint: ["/usr/local/bin/run_go_linter.sh"]
324
+ entrypoint: ["/usr/local/bin/run_go_linter.py"]
325
325
  cap_drop:
326
326
  - ALL
327
327
  security_opt:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentweaver",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "CLI orchestrator for Jira/Codex/Claude engineering workflows",
5
5
  "keywords": [
6
6
  "agent",
@@ -32,8 +32,8 @@
32
32
  "docker-compose.yml",
33
33
  "Dockerfile.codex",
34
34
  "verify_build.sh",
35
- "run_go_tests.sh",
36
- "run_go_linter.sh",
35
+ "run_go_tests.py",
36
+ "run_go_linter.py",
37
37
  "run_go_coverage.sh"
38
38
  ],
39
39
  "publishConfig": {
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ ROOT_DIR = Path(os.environ.get("VERIFY_BUILD_ROOT_DIR") or os.getcwd()).resolve()
12
+
13
+
14
+ def log(message: str) -> None:
15
+ print(message, file=sys.stderr)
16
+
17
+
18
+ def strip_ansi(value: str) -> str:
19
+ import re
20
+
21
+ return re.sub(r"\x1b\[[0-9;]*m", "", value)
22
+
23
+
24
+ def emit_result(
25
+ ok: bool,
26
+ kind: str,
27
+ stage: str,
28
+ exit_code: int,
29
+ summary: str,
30
+ command: str,
31
+ details: dict[str, object] | None = None,
32
+ ) -> None:
33
+ payload = {
34
+ "ok": ok,
35
+ "kind": kind,
36
+ "stage": stage,
37
+ "exitCode": exit_code,
38
+ "summary": summary,
39
+ "command": command,
40
+ "details": details or {},
41
+ }
42
+ print(json.dumps(payload, ensure_ascii=False))
43
+
44
+
45
+ def fail(exit_code: int, summary: str, command: str, details: dict[str, object] | None = None) -> "Never":
46
+ emit_result(False, "linter", "run_go_linter", exit_code, summary, command, details)
47
+ raise SystemExit(exit_code)
48
+
49
+
50
+ def require_cmd(command: str) -> None:
51
+ if shutil.which(command):
52
+ return
53
+ fail(
54
+ 2,
55
+ f"Missing required command: {command}",
56
+ command,
57
+ {"failedStep": "require_cmd", "missingCommand": command},
58
+ )
59
+
60
+
61
+ def collect_issues(output: str) -> list[str]:
62
+ issues: list[str] = []
63
+ for raw_line in output.splitlines():
64
+ cleaned = strip_ansi(raw_line).rstrip("\r").strip()
65
+ if not cleaned or cleaned.startswith("==>"):
66
+ continue
67
+ issues.append(cleaned)
68
+ return issues
69
+
70
+
71
+ def run_command(argv: list[str]) -> tuple[int, str]:
72
+ completed = subprocess.run(
73
+ argv,
74
+ cwd=ROOT_DIR,
75
+ stdout=subprocess.PIPE,
76
+ stderr=subprocess.STDOUT,
77
+ text=True,
78
+ errors="replace",
79
+ check=False,
80
+ )
81
+ output = completed.stdout or ""
82
+ if output:
83
+ print(output, end="" if output.endswith("\n") else "\n", file=sys.stderr)
84
+ return completed.returncode, output
85
+
86
+
87
+ def run_step(label: str, argv: list[str], failed_step: str, summary: str) -> None:
88
+ log(label)
89
+ exit_code, output = run_command(argv)
90
+ if exit_code == 0:
91
+ return
92
+
93
+ fail(
94
+ exit_code,
95
+ summary,
96
+ " ".join(argv),
97
+ {
98
+ "failedStep": failed_step,
99
+ "tool": argv[0],
100
+ "raw": output,
101
+ "issues": collect_issues(output),
102
+ },
103
+ )
104
+
105
+
106
+ def main() -> int:
107
+ require_cmd("go")
108
+ require_cmd("golangci-lint")
109
+
110
+ os.chdir(ROOT_DIR)
111
+
112
+ run_step("==> Generating code (go generate ./...)", ["go", "generate", "./..."], "go-generate", "go generate failed")
113
+ run_step("==> Running linter (golangci-lint run)", ["golangci-lint", "run"], "golangci-lint", "golangci-lint failed")
114
+
115
+ emit_result(
116
+ True,
117
+ "linter",
118
+ "run_go_linter",
119
+ 0,
120
+ "Linter checks passed",
121
+ "go generate ./... && golangci-lint run",
122
+ {"steps": ["go-generate", "golangci-lint"]},
123
+ )
124
+ return 0
125
+
126
+
127
+ if __name__ == "__main__":
128
+ raise SystemExit(main())