@kody-ade/kody-engine-lite 0.1.67 → 0.1.68

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/bin/cli.js CHANGED
@@ -679,6 +679,45 @@ function submitPRReview(prNumber, body, event) {
679
679
  logger.warn(` Failed to submit PR review: ${err}`);
680
680
  }
681
681
  }
682
+ function getCIFailureLogs(runId, maxLength = 8e3) {
683
+ try {
684
+ const logsOutput = gh([
685
+ "run",
686
+ "view",
687
+ String(runId),
688
+ "--log-failed"
689
+ ]);
690
+ if (!logsOutput) return null;
691
+ const truncated = logsOutput.slice(-maxLength);
692
+ const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
693
+ return `${prefix}${truncated}`;
694
+ } catch (err) {
695
+ logger.warn(` Failed to get CI failure logs for run ${runId}: ${err}`);
696
+ return null;
697
+ }
698
+ }
699
+ function getLatestFailedRunForBranch(branch) {
700
+ try {
701
+ const output = gh([
702
+ "run",
703
+ "list",
704
+ "--branch",
705
+ branch,
706
+ "--status",
707
+ "failure",
708
+ "--limit",
709
+ "1",
710
+ "--json",
711
+ "databaseId",
712
+ "--jq",
713
+ ".[0].databaseId"
714
+ ]);
715
+ return output.trim() || null;
716
+ } catch (err) {
717
+ logger.warn(` Failed to get latest failed run for branch ${branch}: ${err}`);
718
+ return null;
719
+ }
720
+ }
682
721
  function getLatestKodyReviewComment(prNumber) {
683
722
  try {
684
723
  const output = gh([
@@ -3203,13 +3242,14 @@ function parseArgs() {
3203
3242
  kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
3204
3243
  kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
3205
3244
  kody fix --task-id <id> [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
3245
+ kody fix-ci [--pr-number <n>] [--ci-run-id <id>] [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
3206
3246
  kody review [--pr-number <n>] [--issue-number <n>] [--cwd <path>] [--local]
3207
3247
  kody status --task-id <id> [--cwd <path>]
3208
3248
  kody --help`);
3209
3249
  process.exit(0);
3210
3250
  }
3211
3251
  const command2 = args2[0];
3212
- if (!["run", "rerun", "fix", "status", "review"].includes(command2)) {
3252
+ if (!["run", "rerun", "fix", "fix-ci", "status", "review"].includes(command2)) {
3213
3253
  console.error(`Unknown command: ${command2}`);
3214
3254
  process.exit(1);
3215
3255
  }
@@ -3227,7 +3267,8 @@ function parseArgs() {
3227
3267
  prNumber: prStr ? parseInt(prStr, 10) : void 0,
3228
3268
  feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
3229
3269
  local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
3230
- complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
3270
+ complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY,
3271
+ ciRunId: getArg(args2, "--ci-run-id") ?? process.env.CI_RUN_ID
3231
3272
  };
3232
3273
  }
3233
3274
  var isCI2;
@@ -3463,7 +3504,7 @@ async function main() {
3463
3504
  setGhCwd(projectDir);
3464
3505
  logger.info(`Working directory: ${projectDir}`);
3465
3506
  }
3466
- const isPRFix = input.command === "fix" && !!input.prNumber;
3507
+ const isPRFix = (input.command === "fix" || input.command === "fix-ci") && !!input.prNumber;
3467
3508
  if (input.issueNumber && input.command !== "review" && !isPRFix) {
3468
3509
  const taskAction = resolveForIssue(input.issueNumber, projectDir);
3469
3510
  logger.info(`Task action: ${taskAction.action}`);
@@ -3497,7 +3538,7 @@ async function main() {
3497
3538
  let taskId = input.taskId;
3498
3539
  if (!taskId) {
3499
3540
  if (isPRFix) {
3500
- taskId = `fix-pr-${input.prNumber}-${generateTaskId()}`;
3541
+ taskId = `${input.command === "fix-ci" ? "fixci" : "fix"}-pr-${input.prNumber}-${generateTaskId()}`;
3501
3542
  } else if (input.issueNumber) {
3502
3543
  taskId = `${input.issueNumber}-${generateTaskId()}`;
3503
3544
  } else if (input.command === "run" && input.task) {
@@ -3615,9 +3656,40 @@ ${issue.body ?? ""}`;
3615
3656
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
3616
3657
  process.exit(1);
3617
3658
  }
3618
- if (input.command === "fix" && !input.fromStage) {
3659
+ if ((input.command === "fix" || input.command === "fix-ci") && !input.fromStage) {
3619
3660
  input.fromStage = "build";
3620
3661
  }
3662
+ if (input.command === "fix-ci" && input.prNumber) {
3663
+ let ciRunId = input.ciRunId;
3664
+ if (!ciRunId && input.feedback) {
3665
+ const match = input.feedback.match(/Run ID:\s*(\d+)/);
3666
+ ciRunId = match?.[1];
3667
+ }
3668
+ if (!ciRunId) {
3669
+ const prDetails = getPRDetails(input.prNumber);
3670
+ if (prDetails) {
3671
+ ciRunId = getLatestFailedRunForBranch(prDetails.headBranch) ?? void 0;
3672
+ }
3673
+ }
3674
+ if (ciRunId) {
3675
+ const ciLogs = getCIFailureLogs(ciRunId);
3676
+ if (ciLogs) {
3677
+ logger.info(` Found CI failure logs for run ${ciRunId}, injecting as feedback`);
3678
+ const ciContext = `## CI Failure Logs (run ${ciRunId})
3679
+
3680
+ The CI pipeline failed. Fix the code to make CI pass.
3681
+
3682
+ \`\`\`
3683
+ ${ciLogs}
3684
+ \`\`\``;
3685
+ input.feedback = input.feedback ? `${ciContext}
3686
+
3687
+ ## Additional context
3688
+
3689
+ ${input.feedback}` : ciContext;
3690
+ }
3691
+ }
3692
+ }
3621
3693
  if (input.command === "fix" && input.prNumber) {
3622
3694
  const reviewComment = getLatestKodyReviewComment(input.prNumber);
3623
3695
  if (reviewComment) {
@@ -3667,7 +3739,7 @@ ${input.feedback}` : reviewContext;
3667
3739
  projectDir,
3668
3740
  runners,
3669
3741
  input: {
3670
- mode: input.command === "rerun" || input.command === "fix" ? "rerun" : "full",
3742
+ mode: input.command === "rerun" || input.command === "fix" || input.command === "fix-ci" ? "rerun" : "full",
3671
3743
  fromStage: input.fromStage,
3672
3744
  dryRun: input.dryRun,
3673
3745
  issueNumber: input.issueNumber,
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.68",
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
+ }
@@ -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'