@kody-ade/kody-engine-lite 0.1.67 → 0.1.69
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/dist/agent-runner.d.ts +4 -0
- package/dist/agent-runner.js +122 -0
- package/dist/bin/cli.js +203 -13
- package/dist/ci/parse-inputs.d.ts +6 -0
- package/dist/ci/parse-inputs.js +76 -0
- package/dist/ci/parse-safety.d.ts +6 -0
- package/dist/ci/parse-safety.js +22 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.js +42 -0
- package/dist/cli/litellm.d.ts +2 -0
- package/dist/cli/litellm.js +85 -0
- package/dist/cli/task-resolution.d.ts +2 -0
- package/dist/cli/task-resolution.js +41 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +72 -0
- package/dist/context.d.ts +4 -0
- package/dist/context.js +83 -0
- package/dist/definitions.d.ts +3 -0
- package/dist/definitions.js +59 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.js +236 -0
- package/dist/git-utils.d.ts +13 -0
- package/dist/git-utils.js +174 -0
- package/dist/github-api.d.ts +14 -0
- package/dist/github-api.js +114 -0
- package/dist/kody-utils.d.ts +1 -0
- package/dist/kody-utils.js +9 -0
- package/dist/learning/auto-learn.d.ts +2 -0
- package/dist/learning/auto-learn.js +169 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.js +51 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.js +20 -0
- package/dist/observer.d.ts +9 -0
- package/dist/observer.js +80 -0
- package/dist/pipeline/complexity.d.ts +3 -0
- package/dist/pipeline/complexity.js +12 -0
- package/dist/pipeline/executor-registry.d.ts +3 -0
- package/dist/pipeline/executor-registry.js +20 -0
- package/dist/pipeline/hooks.d.ts +17 -0
- package/dist/pipeline/hooks.js +110 -0
- package/dist/pipeline/questions.d.ts +2 -0
- package/dist/pipeline/questions.js +44 -0
- package/dist/pipeline/runner-selection.d.ts +2 -0
- package/dist/pipeline/runner-selection.js +13 -0
- package/dist/pipeline/state.d.ts +4 -0
- package/dist/pipeline/state.js +37 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.js +213 -0
- package/dist/preflight.d.ts +1 -0
- package/dist/preflight.js +69 -0
- package/dist/retrospective.d.ts +26 -0
- package/dist/retrospective.js +211 -0
- package/dist/stages/agent.d.ts +2 -0
- package/dist/stages/agent.js +94 -0
- package/dist/stages/gate.d.ts +2 -0
- package/dist/stages/gate.js +32 -0
- package/dist/stages/review.d.ts +2 -0
- package/dist/stages/review.js +32 -0
- package/dist/stages/ship.d.ts +3 -0
- package/dist/stages/ship.js +154 -0
- package/dist/stages/verify.d.ts +2 -0
- package/dist/stages/verify.js +94 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +1 -0
- package/dist/validators.d.ts +8 -0
- package/dist/validators.js +42 -0
- package/dist/verify-runner.d.ts +11 -0
- package/dist/verify-runner.js +110 -0
- package/package.json +9 -8
- package/prompts/plan.md +18 -1
- package/templates/kody.yml +82 -3
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type StageName = "taskify" | "plan" | "build" | "verify" | "review" | "review-fix" | "ship";
|
|
2
|
+
export type StageType = "agent" | "gate" | "deterministic";
|
|
3
|
+
export type PipelineState = "pending" | "running" | "completed" | "failed" | "timeout";
|
|
4
|
+
export interface StageDefinition {
|
|
5
|
+
name: StageName;
|
|
6
|
+
type: StageType;
|
|
7
|
+
modelTier: "cheap" | "mid" | "strong";
|
|
8
|
+
timeout: number;
|
|
9
|
+
maxRetries: number;
|
|
10
|
+
outputFile?: string;
|
|
11
|
+
retryWithAgent?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface StageState {
|
|
14
|
+
state: PipelineState;
|
|
15
|
+
startedAt?: string;
|
|
16
|
+
completedAt?: string;
|
|
17
|
+
retries: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
outputFile?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PipelineStatus {
|
|
22
|
+
taskId: string;
|
|
23
|
+
state: "running" | "completed" | "failed";
|
|
24
|
+
stages: Record<StageName, StageState>;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
}
|
|
28
|
+
export interface StageResult {
|
|
29
|
+
outcome: "completed" | "failed" | "timed_out";
|
|
30
|
+
outputFile?: string;
|
|
31
|
+
error?: string;
|
|
32
|
+
retries: number;
|
|
33
|
+
}
|
|
34
|
+
export interface AgentResult {
|
|
35
|
+
outcome: "completed" | "failed" | "timed_out";
|
|
36
|
+
output?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface AgentRunnerOptions {
|
|
40
|
+
cwd?: string;
|
|
41
|
+
env?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
export interface AgentRunner {
|
|
44
|
+
run(stageName: string, prompt: string, model: string, timeout: number, taskDir: string, options?: AgentRunnerOptions): Promise<AgentResult>;
|
|
45
|
+
healthCheck(): Promise<boolean>;
|
|
46
|
+
}
|
|
47
|
+
export interface PipelineContext {
|
|
48
|
+
taskId: string;
|
|
49
|
+
taskDir: string;
|
|
50
|
+
projectDir: string;
|
|
51
|
+
runners: Record<string, AgentRunner>;
|
|
52
|
+
input: {
|
|
53
|
+
mode: "full" | "rerun" | "status";
|
|
54
|
+
fromStage?: string;
|
|
55
|
+
dryRun?: boolean;
|
|
56
|
+
issueNumber?: number;
|
|
57
|
+
feedback?: string;
|
|
58
|
+
local?: boolean;
|
|
59
|
+
complexity?: "low" | "medium" | "high";
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ValidationResult {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function stripFences(content: string): string;
|
|
6
|
+
export declare function validateTaskJson(content: string): ValidationResult;
|
|
7
|
+
export declare function validatePlanMd(content: string): ValidationResult;
|
|
8
|
+
export declare function validateReviewMd(content: string): ValidationResult;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const REQUIRED_TASK_FIELDS = [
|
|
2
|
+
"task_type",
|
|
3
|
+
"title",
|
|
4
|
+
"description",
|
|
5
|
+
"scope",
|
|
6
|
+
"risk_level",
|
|
7
|
+
];
|
|
8
|
+
export function stripFences(content) {
|
|
9
|
+
return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
10
|
+
}
|
|
11
|
+
export function validateTaskJson(content) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(stripFences(content));
|
|
14
|
+
for (const field of REQUIRED_TASK_FIELDS) {
|
|
15
|
+
if (!(field in parsed)) {
|
|
16
|
+
return { valid: false, error: `Missing field: ${field}` };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { valid: true };
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
return {
|
|
23
|
+
valid: false,
|
|
24
|
+
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function validatePlanMd(content) {
|
|
29
|
+
if (content.length < 10) {
|
|
30
|
+
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
31
|
+
}
|
|
32
|
+
if (!/^##\s+\w+/m.test(content)) {
|
|
33
|
+
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
34
|
+
}
|
|
35
|
+
return { valid: true };
|
|
36
|
+
}
|
|
37
|
+
export function validateReviewMd(content) {
|
|
38
|
+
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
39
|
+
return { valid: true };
|
|
40
|
+
}
|
|
41
|
+
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface VerifyResult {
|
|
2
|
+
pass: boolean;
|
|
3
|
+
errors: string[];
|
|
4
|
+
summary: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Parse a command string into [executable, ...args], respecting quoted arguments.
|
|
8
|
+
* e.g., 'pnpm -s "test:unit"' → ["pnpm", "-s", "test:unit"]
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseCommand(cmd: string): string[];
|
|
11
|
+
export declare function runQualityGates(taskDir: string, projectRoot?: string): VerifyResult;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import { getProjectConfig, VERIFY_COMMAND_TIMEOUT_MS } from "./config.js";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
function isExecError(err) {
|
|
5
|
+
return typeof err === "object" && err !== null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse a command string into [executable, ...args], respecting quoted arguments.
|
|
9
|
+
* e.g., 'pnpm -s "test:unit"' → ["pnpm", "-s", "test:unit"]
|
|
10
|
+
*/
|
|
11
|
+
export function parseCommand(cmd) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
let current = "";
|
|
14
|
+
let inQuote = null;
|
|
15
|
+
for (const ch of cmd) {
|
|
16
|
+
if (inQuote) {
|
|
17
|
+
if (ch === inQuote) {
|
|
18
|
+
inQuote = null;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
current += ch;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (ch === '"' || ch === "'") {
|
|
25
|
+
inQuote = ch;
|
|
26
|
+
}
|
|
27
|
+
else if (/\s/.test(ch)) {
|
|
28
|
+
if (current) {
|
|
29
|
+
parts.push(current);
|
|
30
|
+
current = "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
current += ch;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (current)
|
|
38
|
+
parts.push(current);
|
|
39
|
+
if (inQuote)
|
|
40
|
+
logger.warn(`Unclosed quote in command: ${cmd}`);
|
|
41
|
+
return parts;
|
|
42
|
+
}
|
|
43
|
+
function runCommand(cmd, cwd, timeout) {
|
|
44
|
+
const parts = parseCommand(cmd);
|
|
45
|
+
if (parts.length === 0) {
|
|
46
|
+
return { success: true, output: "", timedOut: false };
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const output = execFileSync(parts[0], parts.slice(1), {
|
|
50
|
+
cwd,
|
|
51
|
+
timeout,
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
54
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
55
|
+
});
|
|
56
|
+
return { success: true, output: output ?? "", timedOut: false };
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const stdout = isExecError(err) ? err.stdout ?? "" : "";
|
|
60
|
+
const stderr = isExecError(err) ? err.stderr ?? "" : "";
|
|
61
|
+
const killed = isExecError(err) ? !!err.killed : false;
|
|
62
|
+
return { success: false, output: `${stdout}${stderr}`, timedOut: killed };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function parseErrors(output) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
for (const line of output.split("\n")) {
|
|
68
|
+
if (/error|Error|ERROR|failed|Failed|FAIL|warning:|Warning:|WARN/i.test(line)) {
|
|
69
|
+
errors.push(line.slice(0, 500));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return errors;
|
|
73
|
+
}
|
|
74
|
+
function extractSummary(output, cmdName) {
|
|
75
|
+
const summaryPatterns = /Test Suites|Tests|Coverage|ERRORS|FAILURES|success|completed/i;
|
|
76
|
+
const lines = output.split("\n").filter((l) => summaryPatterns.test(l));
|
|
77
|
+
return lines.slice(-3).map((l) => `[${cmdName}] ${l.trim()}`);
|
|
78
|
+
}
|
|
79
|
+
export function runQualityGates(taskDir, projectRoot) {
|
|
80
|
+
const config = getProjectConfig();
|
|
81
|
+
const cwd = projectRoot ?? process.cwd();
|
|
82
|
+
const allErrors = [];
|
|
83
|
+
const allSummary = [];
|
|
84
|
+
let allPass = true;
|
|
85
|
+
const commands = [
|
|
86
|
+
{ name: "typecheck", cmd: config.quality.typecheck },
|
|
87
|
+
{ name: "test", cmd: config.quality.testUnit },
|
|
88
|
+
];
|
|
89
|
+
if (config.quality.lint) {
|
|
90
|
+
commands.push({ name: "lint", cmd: config.quality.lint });
|
|
91
|
+
}
|
|
92
|
+
for (const { name, cmd } of commands) {
|
|
93
|
+
if (!cmd)
|
|
94
|
+
continue;
|
|
95
|
+
logger.info(` Running ${name}: ${cmd}`);
|
|
96
|
+
const result = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
|
|
97
|
+
if (result.timedOut) {
|
|
98
|
+
allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1000}s`);
|
|
99
|
+
allPass = false;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!result.success) {
|
|
103
|
+
allPass = false;
|
|
104
|
+
const errors = parseErrors(result.output);
|
|
105
|
+
allErrors.push(...errors.map((e) => `[${name}] ${e}`));
|
|
106
|
+
}
|
|
107
|
+
allSummary.push(...extractSummary(result.output, name));
|
|
108
|
+
}
|
|
109
|
+
return { pass: allPass, errors: allErrors, summary: allSummary };
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine-lite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"description": "Autonomous SDLC pipeline: Kody orchestration + Claude Code + LiteLLM",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
"templates",
|
|
14
14
|
"kody.config.schema.json"
|
|
15
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"kody": "tsx src/entry.ts",
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "pnpm build"
|
|
22
|
+
},
|
|
16
23
|
"dependencies": {
|
|
17
24
|
"dotenv": "^16.4.7"
|
|
18
25
|
},
|
|
@@ -25,11 +32,5 @@
|
|
|
25
32
|
},
|
|
26
33
|
"engines": {
|
|
27
34
|
"node": ">=22"
|
|
28
|
-
},
|
|
29
|
-
"scripts": {
|
|
30
|
-
"kody": "tsx src/entry.ts",
|
|
31
|
-
"build": "tsup",
|
|
32
|
-
"test": "vitest run",
|
|
33
|
-
"typecheck": "tsc --noEmit"
|
|
34
35
|
}
|
|
35
|
-
}
|
|
36
|
+
}
|
package/prompts/plan.md
CHANGED
|
@@ -7,7 +7,16 @@ tools: [read, glob, grep]
|
|
|
7
7
|
|
|
8
8
|
You are a planning agent following the Superpowers Writing Plans methodology.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## MANDATORY: Pattern Discovery Before Planning
|
|
11
|
+
|
|
12
|
+
Before writing ANY plan, you MUST search for existing patterns in the codebase:
|
|
13
|
+
|
|
14
|
+
1. **Find similar implementations** — Grep/Glob for how the same problem is already solved elsewhere. E.g., if the task involves localization, search for how other collections handle localization. If adding auth, find existing auth patterns.
|
|
15
|
+
2. **Reuse existing patterns** — If the codebase already solves a similar problem, your plan MUST follow that pattern unless there's a strong reason not to (document the reason in Questions).
|
|
16
|
+
3. **Check decisions.md** — If `.kody/memory/decisions.md` exists, read it for prior architectural decisions that may apply.
|
|
17
|
+
4. **Never invent when you can reuse** — Proposing a new pattern when an existing one covers the use case is a planning failure.
|
|
18
|
+
|
|
19
|
+
After pattern discovery, examine the codebase to understand existing code structure, patterns, and conventions. Use Read, Glob, and Grep.
|
|
11
20
|
|
|
12
21
|
Output a markdown plan. Start with the steps, then optionally add a Questions section at the end.
|
|
13
22
|
|
|
@@ -45,4 +54,12 @@ Questions rules:
|
|
|
45
54
|
Good questions: "Recommend middleware pattern vs wrapper — middleware is simpler but wrapper allows caching. Approve middleware?"
|
|
46
55
|
Bad questions: "What should I name the function?", "Should I add tests?"
|
|
47
56
|
|
|
57
|
+
## Pattern Discovery Report
|
|
58
|
+
|
|
59
|
+
After the plan steps and before Questions, include a brief report of what existing patterns you found and how your plan reuses them:
|
|
60
|
+
|
|
61
|
+
## Existing Patterns Found
|
|
62
|
+
- <pattern found>: <how it's reused in the plan>
|
|
63
|
+
- <if no existing patterns found, explain what you searched for>
|
|
64
|
+
|
|
48
65
|
{{TASK_CONTEXT}}
|
package/templates/kody.yml
CHANGED
|
@@ -28,12 +28,16 @@ on:
|
|
|
28
28
|
pull_request_review:
|
|
29
29
|
types: [submitted]
|
|
30
30
|
|
|
31
|
+
workflow_run:
|
|
32
|
+
workflows: ["CI"]
|
|
33
|
+
types: [completed]
|
|
34
|
+
|
|
31
35
|
push:
|
|
32
36
|
branches: [main, dev]
|
|
33
37
|
paths: ["src/**", "kody.config.json", "package.json"]
|
|
34
38
|
|
|
35
39
|
concurrency:
|
|
36
|
-
group: kody-${{ github.event.inputs.task_id || github.event.issue.number || github.event.pull_request.number || github.sha }}
|
|
40
|
+
group: kody-${{ github.event.inputs.task_id || github.event.issue.number || github.event.pull_request.number || github.event.workflow_run.id || github.sha }}
|
|
37
41
|
cancel-in-progress: false
|
|
38
42
|
|
|
39
43
|
permissions:
|
|
@@ -56,6 +60,7 @@ jobs:
|
|
|
56
60
|
issue_number: ${{ steps.parse.outputs.issue_number }}
|
|
57
61
|
pr_number: ${{ steps.parse.outputs.pr_number }}
|
|
58
62
|
feedback: ${{ steps.parse.outputs.feedback }}
|
|
63
|
+
ci_run_id: ${{ steps.parse.outputs.ci_run_id }}
|
|
59
64
|
valid: ${{ steps.parse.outputs.valid }}
|
|
60
65
|
steps:
|
|
61
66
|
- uses: actions/setup-node@v4
|
|
@@ -109,7 +114,7 @@ jobs:
|
|
|
109
114
|
|
|
110
115
|
# Validate mode
|
|
111
116
|
case "$MODE" in
|
|
112
|
-
full|rerun|fix|status|approve|review|bootstrap) ;;
|
|
117
|
+
full|rerun|fix|fix-ci|status|approve|review|bootstrap) ;;
|
|
113
118
|
*)
|
|
114
119
|
# If first arg isn't a mode, it might be a task-id or nothing
|
|
115
120
|
if [ -n "$MODE" ] && [ "$MODE" != "" ]; then
|
|
@@ -139,6 +144,15 @@ jobs:
|
|
|
139
144
|
# Leave TASK_ID empty — entry.ts finds latest task for issue
|
|
140
145
|
fi
|
|
141
146
|
|
|
147
|
+
# fix-ci: extract body as feedback + CI run ID
|
|
148
|
+
if [ "$MODE" = "fix-ci" ]; then
|
|
149
|
+
FIX_CI_BODY=$(echo "$BODY" | sed -n '/\(@kody\|\/kody\)\s*fix-ci/,$p' | tail -n +2)
|
|
150
|
+
if [ -n "$FIX_CI_BODY" ]; then
|
|
151
|
+
FEEDBACK="$FIX_CI_BODY"
|
|
152
|
+
fi
|
|
153
|
+
CI_RUN_ID=$(echo "$FIX_CI_BODY" | grep -oP 'Run ID:\s*\K\d+' || echo "")
|
|
154
|
+
fi
|
|
155
|
+
|
|
142
156
|
# Bootstrap mode: set task-id and skip normal pipeline
|
|
143
157
|
if [ "$MODE" = "bootstrap" ]; then
|
|
144
158
|
TASK_ID="bootstrap-$(date +%y%m%d-%H%M%S)"
|
|
@@ -170,6 +184,7 @@ jobs:
|
|
|
170
184
|
echo "$FEEDBACK"
|
|
171
185
|
echo "KODY_EOF"
|
|
172
186
|
} >> $GITHUB_OUTPUT
|
|
187
|
+
echo "ci_run_id=${CI_RUN_ID:-}" >> $GITHUB_OUTPUT
|
|
173
188
|
echo "valid=true" >> $GITHUB_OUTPUT
|
|
174
189
|
|
|
175
190
|
# ─── Orchestrate ─────────────────────────────────────────────────────────────
|
|
@@ -198,7 +213,7 @@ jobs:
|
|
|
198
213
|
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
|
|
199
214
|
|
|
200
215
|
- name: Checkout PR branch (for fix/rerun/review on PRs)
|
|
201
|
-
if: github.event.issue.pull_request && (needs.parse.outputs.mode == 'fix' || needs.parse.outputs.mode == 'rerun' || needs.parse.outputs.mode == 'review')
|
|
216
|
+
if: github.event.issue.pull_request && (needs.parse.outputs.mode == 'fix' || needs.parse.outputs.mode == 'fix-ci' || needs.parse.outputs.mode == 'rerun' || needs.parse.outputs.mode == 'review')
|
|
202
217
|
env:
|
|
203
218
|
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
|
|
204
219
|
run: |
|
|
@@ -243,6 +258,7 @@ jobs:
|
|
|
243
258
|
ISSUE_NUMBER: ${{ github.event.inputs.issue_number || needs.parse.outputs.issue_number }}
|
|
244
259
|
PR_NUMBER: ${{ needs.parse.outputs.pr_number }}
|
|
245
260
|
FEEDBACK: ${{ github.event.inputs.feedback || needs.parse.outputs.feedback }}
|
|
261
|
+
CI_RUN_ID: ${{ needs.parse.outputs.ci_run_id }}
|
|
246
262
|
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
|
247
263
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
248
264
|
run: |
|
|
@@ -253,6 +269,7 @@ jobs:
|
|
|
253
269
|
CMD="run"
|
|
254
270
|
[ "$MODE" = "rerun" ] && CMD="rerun"
|
|
255
271
|
[ "$MODE" = "fix" ] && CMD="fix"
|
|
272
|
+
[ "$MODE" = "fix-ci" ] && CMD="fix-ci"
|
|
256
273
|
[ "$MODE" = "review" ] && CMD="review"
|
|
257
274
|
ARGS="--issue-number $ISSUE_NUMBER"
|
|
258
275
|
[ -n "$TASK_ID" ] && ARGS="$ARGS --task-id $TASK_ID"
|
|
@@ -338,6 +355,68 @@ jobs:
|
|
|
338
355
|
});
|
|
339
356
|
}
|
|
340
357
|
|
|
358
|
+
# ─── Fix-CI Auto-trigger (workflow_run trigger) ─────────────────────────────
|
|
359
|
+
fix-ci-trigger:
|
|
360
|
+
if: >-
|
|
361
|
+
github.event_name == 'workflow_run' &&
|
|
362
|
+
github.event.workflow_run.conclusion == 'failure' &&
|
|
363
|
+
github.event.workflow_run.event == 'pull_request' &&
|
|
364
|
+
github.event.workflow_run.pull_requests[0]
|
|
365
|
+
runs-on: ubuntu-latest
|
|
366
|
+
timeout-minutes: 5
|
|
367
|
+
permissions:
|
|
368
|
+
issues: write
|
|
369
|
+
pull-requests: write
|
|
370
|
+
steps:
|
|
371
|
+
- name: Check loop guard and post fix-ci comment
|
|
372
|
+
uses: actions/github-script@v7
|
|
373
|
+
with:
|
|
374
|
+
script: |
|
|
375
|
+
const pr = context.payload.workflow_run.pull_requests[0];
|
|
376
|
+
const prNumber = pr.number;
|
|
377
|
+
|
|
378
|
+
// Check recent comments for existing fix-ci attempt (last 24h)
|
|
379
|
+
const comments = await github.rest.issues.listComments({
|
|
380
|
+
owner: context.repo.owner,
|
|
381
|
+
repo: context.repo.repo,
|
|
382
|
+
issue_number: prNumber,
|
|
383
|
+
per_page: 30,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
387
|
+
const recentFixCi = comments.data.filter(
|
|
388
|
+
(c) => c.body.includes('@kody fix-ci') && new Date(c.created_at) > oneDayAgo
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
if (recentFixCi.length >= 1) {
|
|
392
|
+
core.info('Loop guard: @kody fix-ci already commented in last 24h, skipping');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check if last commit was from a bot (kody's previous fix attempt)
|
|
397
|
+
const commits = await github.rest.pulls.listCommits({
|
|
398
|
+
owner: context.repo.owner,
|
|
399
|
+
repo: context.repo.repo,
|
|
400
|
+
pull_number: prNumber,
|
|
401
|
+
per_page: 1,
|
|
402
|
+
});
|
|
403
|
+
const lastAuthor = commits.data[commits.data.length - 1]?.commit?.author?.name;
|
|
404
|
+
if (lastAuthor === 'github-actions[bot]' || lastAuthor === 'kody[bot]') {
|
|
405
|
+
core.info('Loop guard: last commit from bot, skipping');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Post fix-ci comment
|
|
410
|
+
const runId = context.payload.workflow_run.id;
|
|
411
|
+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
|
|
412
|
+
await github.rest.issues.createComment({
|
|
413
|
+
owner: context.repo.owner,
|
|
414
|
+
repo: context.repo.repo,
|
|
415
|
+
issue_number: prNumber,
|
|
416
|
+
body: `@kody fix-ci\nCI failed: [View logs](${runUrl})\nRun ID: ${runId}`,
|
|
417
|
+
});
|
|
418
|
+
core.info(`Posted @kody fix-ci on PR #${prNumber} for run ${runId}`);
|
|
419
|
+
|
|
341
420
|
# ─── Smoke Test (push trigger) ──────────────────────────────────────────────
|
|
342
421
|
smoke-test:
|
|
343
422
|
if: github.event_name == 'push'
|