@kody-ade/kody-engine-lite 0.1.66 → 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,
@@ -4201,7 +4273,7 @@ function ghComment(issueNumber, body, cwd) {
4201
4273
  } catch {
4202
4274
  }
4203
4275
  }
4204
- function bootstrapCommand() {
4276
+ function bootstrapCommand(opts = { force: false }) {
4205
4277
  const cwd = process.cwd();
4206
4278
  const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
4207
4279
  console.log(`
@@ -4275,6 +4347,23 @@ ${existingFiles.join(", ")}
4275
4347
  fs22.mkdirSync(memoryDir, { recursive: true });
4276
4348
  const archPath = path21.join(memoryDir, "architecture.md");
4277
4349
  const conventionsPath = path21.join(memoryDir, "conventions.md");
4350
+ const existingArch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
4351
+ const existingConv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
4352
+ const hasExisting = !!(existingArch || existingConv);
4353
+ const extendInstruction = hasExisting && !opts.force ? `
4354
+ ## Existing Documentation (EXTEND, do not replace)
4355
+ You are UPDATING existing documentation. Follow these rules strictly:
4356
+ - PRESERVE all existing sections and content that are still accurate
4357
+ - REMOVE only lines that reference files, patterns, or dependencies that no longer exist in the project
4358
+ - APPEND new sections or lines for newly discovered patterns, files, or conventions
4359
+ - Do NOT rewrite sections that are still correct \u2014 keep them verbatim
4360
+
4361
+ ### Existing architecture.md:
4362
+ ${existingArch}
4363
+
4364
+ ### Existing conventions.md:
4365
+ ${existingConv}
4366
+ ` : "";
4278
4367
  const memoryPrompt = `You are analyzing a project to generate documentation for an autonomous SDLC pipeline.
4279
4368
 
4280
4369
  Given this project context, output ONLY a JSON object with EXACTLY this structure:
@@ -4294,7 +4383,7 @@ Rules for conventions (markdown string):
4294
4383
  - Extract actual patterns from the project
4295
4384
  - If CLAUDE.md exists, reference it
4296
4385
  - Keep under 30 lines
4297
-
4386
+ ${extendInstruction}
4298
4387
  Output ONLY valid JSON. No markdown fences. No explanation.
4299
4388
 
4300
4389
  ${repoContext}`;
@@ -4352,11 +4441,16 @@ ${detected.join("\n")}
4352
4441
  console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
4353
4442
  continue;
4354
4443
  }
4444
+ const stepOutputPath = path21.join(stepsDir, `${stage}.md`);
4445
+ if (fs22.existsSync(stepOutputPath) && !opts.force) {
4446
+ console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
4447
+ continue;
4448
+ }
4355
4449
  const defaultPrompt = fs22.readFileSync(templatePath, "utf-8");
4356
4450
  const contextPlaceholder = "{{TASK_CONTEXT}}";
4357
4451
  const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
4358
4452
  if (placeholderIdx === -1) {
4359
- fs22.copyFileSync(templatePath, path21.join(stepsDir, `${stage}.md`));
4453
+ fs22.copyFileSync(templatePath, stepOutputPath);
4360
4454
  stepCount++;
4361
4455
  console.log(` \u2713 ${stage}.md`);
4362
4456
  continue;
@@ -4413,12 +4507,12 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
4413
4507
  let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
4414
4508
  cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
4415
4509
  const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
4416
- fs22.writeFileSync(path21.join(stepsDir, `${stage}.md`), finalPrompt);
4510
+ fs22.writeFileSync(stepOutputPath, finalPrompt);
4417
4511
  stepCount++;
4418
4512
  console.log(` \u2713 ${stage}.md`);
4419
4513
  } catch {
4420
4514
  console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
4421
- fs22.copyFileSync(templatePath, path21.join(stepsDir, `${stage}.md`));
4515
+ fs22.copyFileSync(templatePath, stepOutputPath);
4422
4516
  stepCount++;
4423
4517
  }
4424
4518
  }
@@ -4685,8 +4779,26 @@ function installSkillsForProject(cwd) {
4685
4779
  console.log(" \u25CB No skills to install (no frontend framework detected)");
4686
4780
  return [];
4687
4781
  }
4782
+ let installedSkills = {};
4783
+ const lockPath = path21.join(cwd, "skills-lock.json");
4784
+ if (fs22.existsSync(lockPath)) {
4785
+ try {
4786
+ const lock = JSON.parse(fs22.readFileSync(lockPath, "utf-8"));
4787
+ installedSkills = lock.skills ?? {};
4788
+ } catch {
4789
+ }
4790
+ }
4688
4791
  const installedPaths = [];
4689
4792
  for (const skill of skills) {
4793
+ const skillName = skill.package.split("@").pop() ?? "";
4794
+ if (skillName in installedSkills) {
4795
+ console.log(` \u25CB ${skill.label} \u2014 already installed`);
4796
+ const agentPath = `.agents/skills/${skillName}`;
4797
+ const claudePath = `.claude/skills/${skillName}`;
4798
+ if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
4799
+ if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
4800
+ continue;
4801
+ }
4690
4802
  try {
4691
4803
  console.log(` Installing: ${skill.label} (${skill.package})`);
4692
4804
  execFileSync11("npx", ["skills", "add", skill.package, "--yes"], {
@@ -4695,9 +4807,9 @@ function installSkillsForProject(cwd) {
4695
4807
  timeout: 6e4,
4696
4808
  stdio: ["pipe", "pipe", "pipe"]
4697
4809
  });
4698
- const skillName = skill.package.split("@").pop() ?? "";
4699
- const agentPath = `.agents/skills/${skillName}`;
4700
- const claudePath = `.claude/skills/${skillName}`;
4810
+ const skillName2 = skill.package.split("@").pop() ?? "";
4811
+ const agentPath = `.agents/skills/${skillName2}`;
4812
+ const claudePath = `.claude/skills/${skillName2}`;
4701
4813
  if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
4702
4814
  if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
4703
4815
  console.log(` \u2713 ${skill.label}`);
@@ -4712,7 +4824,7 @@ var command = args[0];
4712
4824
  if (command === "init") {
4713
4825
  initCommand({ force: args.includes("--force") });
4714
4826
  } else if (command === "bootstrap") {
4715
- bootstrapCommand();
4827
+ bootstrapCommand({ force: args.includes("--force") });
4716
4828
  } else if (command === "version" || command === "--version" || command === "-v") {
4717
4829
  console.log(getVersion());
4718
4830
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine-lite",
3
- "version": "0.1.66",
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'