@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.
Files changed (72) hide show
  1. package/dist/agent-runner.d.ts +4 -0
  2. package/dist/agent-runner.js +122 -0
  3. package/dist/bin/cli.js +203 -13
  4. package/dist/ci/parse-inputs.d.ts +6 -0
  5. package/dist/ci/parse-inputs.js +76 -0
  6. package/dist/ci/parse-safety.d.ts +6 -0
  7. package/dist/ci/parse-safety.js +22 -0
  8. package/dist/cli/args.d.ts +13 -0
  9. package/dist/cli/args.js +42 -0
  10. package/dist/cli/litellm.d.ts +2 -0
  11. package/dist/cli/litellm.js +85 -0
  12. package/dist/cli/task-resolution.d.ts +2 -0
  13. package/dist/cli/task-resolution.js +41 -0
  14. package/dist/config.d.ts +49 -0
  15. package/dist/config.js +72 -0
  16. package/dist/context.d.ts +4 -0
  17. package/dist/context.js +83 -0
  18. package/dist/definitions.d.ts +3 -0
  19. package/dist/definitions.js +59 -0
  20. package/dist/entry.d.ts +1 -0
  21. package/dist/entry.js +236 -0
  22. package/dist/git-utils.d.ts +13 -0
  23. package/dist/git-utils.js +174 -0
  24. package/dist/github-api.d.ts +14 -0
  25. package/dist/github-api.js +114 -0
  26. package/dist/kody-utils.d.ts +1 -0
  27. package/dist/kody-utils.js +9 -0
  28. package/dist/learning/auto-learn.d.ts +2 -0
  29. package/dist/learning/auto-learn.js +169 -0
  30. package/dist/logger.d.ts +14 -0
  31. package/dist/logger.js +51 -0
  32. package/dist/memory.d.ts +1 -0
  33. package/dist/memory.js +20 -0
  34. package/dist/observer.d.ts +9 -0
  35. package/dist/observer.js +80 -0
  36. package/dist/pipeline/complexity.d.ts +3 -0
  37. package/dist/pipeline/complexity.js +12 -0
  38. package/dist/pipeline/executor-registry.d.ts +3 -0
  39. package/dist/pipeline/executor-registry.js +20 -0
  40. package/dist/pipeline/hooks.d.ts +17 -0
  41. package/dist/pipeline/hooks.js +110 -0
  42. package/dist/pipeline/questions.d.ts +2 -0
  43. package/dist/pipeline/questions.js +44 -0
  44. package/dist/pipeline/runner-selection.d.ts +2 -0
  45. package/dist/pipeline/runner-selection.js +13 -0
  46. package/dist/pipeline/state.d.ts +4 -0
  47. package/dist/pipeline/state.js +37 -0
  48. package/dist/pipeline.d.ts +3 -0
  49. package/dist/pipeline.js +213 -0
  50. package/dist/preflight.d.ts +1 -0
  51. package/dist/preflight.js +69 -0
  52. package/dist/retrospective.d.ts +26 -0
  53. package/dist/retrospective.js +211 -0
  54. package/dist/stages/agent.d.ts +2 -0
  55. package/dist/stages/agent.js +94 -0
  56. package/dist/stages/gate.d.ts +2 -0
  57. package/dist/stages/gate.js +32 -0
  58. package/dist/stages/review.d.ts +2 -0
  59. package/dist/stages/review.js +32 -0
  60. package/dist/stages/ship.d.ts +3 -0
  61. package/dist/stages/ship.js +154 -0
  62. package/dist/stages/verify.d.ts +2 -0
  63. package/dist/stages/verify.js +94 -0
  64. package/dist/types.d.ts +61 -0
  65. package/dist/types.js +1 -0
  66. package/dist/validators.d.ts +8 -0
  67. package/dist/validators.js +42 -0
  68. package/dist/verify-runner.d.ts +11 -0
  69. package/dist/verify-runner.js +110 -0
  70. package/package.json +9 -8
  71. package/prompts/plan.md +18 -1
  72. package/templates/kody.yml +82 -3
@@ -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.67",
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
- Before planning, examine the codebase to understand existing code structure, patterns, and conventions. Use Read, Glob, and Grep.
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}}
@@ -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'