@benzotti/jedi 0.1.39 → 0.1.40

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.
@@ -60,14 +60,13 @@ jobs:
60
60
  - name: Resolve PR branch
61
61
  id: pr
62
62
  run: |
63
- if [ -n "$PR_HEAD_REF" ]; then
64
- echo "branch=$PR_HEAD_REF" >> "$GITHUB_OUTPUT"
65
- elif [ -n "$PR_NUMBER" ]; then
66
- BRANCH=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER --jq '.head.ref')
67
- echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
68
- fi
63
+ bunx @benzotti/jedi@latest action resolve-branch \
64
+ --repo "$REPO" \
65
+ ${PR_HEAD_REF:+--pr-head-ref "$PR_HEAD_REF"} \
66
+ ${PR_NUMBER:+--pr-number "$PR_NUMBER"}
69
67
  env:
70
68
  GH_TOKEN: ${{ github.token }}
69
+ REPO: ${{ github.repository }}
71
70
  PR_HEAD_REF: ${{ github.event.pull_request.head.ref || '' }}
72
71
  PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number || '' }}
73
72
 
@@ -100,81 +99,14 @@ jobs:
100
99
  # Bootstrap Jedi framework if not present (first run or cache miss)
101
100
  - name: Bootstrap Jedi
102
101
  run: |
103
- if [ ! -d ".jdi/framework" ]; then
104
- bunx @benzotti/jedi@latest init --ci
105
- fi
106
- mkdir -p .jdi/persistence
107
-
108
- # If cache was restored from fallback (not exact branch match),
109
- # clear plans and state — they're branch-local, only learnings carry forward
110
- if [ "${{ steps.cache.outputs.cache-hit }}" != "true" ]; then
111
- echo "Cache miss or fallback — clearing plan state"
112
- rm -rf .jdi/plans/*
113
- mkdir -p .jdi/config
114
- printf 'active_plan: null\ncurrent_wave: null\nmode: null\n' > .jdi/config/state.yaml
115
- fi
116
-
117
- # Exclude Jedi working directories from git so they don't get committed
118
- # or conflict with branch checkouts — they live in the cache only
119
- mkdir -p .git/info
120
- for pattern in '.jdi/' '.claude/'; do
121
- grep -qxF "$pattern" .git/info/exclude 2>/dev/null || echo "$pattern" >> .git/info/exclude
122
- done
102
+ bunx @benzotti/jedi@latest action bootstrap --cache-hit "${{ steps.cache.outputs.cache-hit }}"
123
103
 
124
104
  # Fetch shared learnings from an external repo if configured.
125
105
  # This merges cross-project learnings into the local framework before agents run.
126
106
  - name: Fetch shared learnings
127
107
  if: vars.JEDI_LEARNINGS_REPO != ''
128
108
  run: |
129
- LEARNINGS_DIR=".jdi/framework/learnings"
130
- REMOTE_SUBDIR="jedi/learnings"
131
- mkdir -p "$LEARNINGS_DIR"
132
-
133
- # Clone only the learnings directory (sparse checkout)
134
- TMPDIR=$(mktemp -d)
135
- git clone --depth 1 --filter=blob:none --sparse \
136
- "https://x-access-token:${LEARNINGS_TOKEN}@github.com/${LEARNINGS_REPO}.git" \
137
- "$TMPDIR" 2>/dev/null || {
138
- echo "Could not clone learnings repo — continuing without shared learnings"
139
- exit 0
140
- }
141
-
142
- cd "$TMPDIR"
143
- git sparse-checkout set "$REMOTE_SUBDIR" 2>/dev/null || true
144
- cd - > /dev/null
145
-
146
- # Merge learnings from external repo into local framework
147
- if [ -d "$TMPDIR/$REMOTE_SUBDIR" ]; then
148
- for f in "$TMPDIR/$REMOTE_SUBDIR"/*.md; do
149
- [ -f "$f" ] || continue
150
- BASENAME=$(basename "$f")
151
- LOCAL_FILE="$LEARNINGS_DIR/$BASENAME"
152
-
153
- if [ ! -f "$LOCAL_FILE" ]; then
154
- cp "$f" "$LOCAL_FILE"
155
- echo "Loaded shared learning: $BASENAME"
156
- else
157
- # Append lines from remote that aren't already in the local file
158
- MERGED=0
159
- while IFS= read -r line; do
160
- [ -z "$line" ] && continue
161
- if ! grep -qFx "$line" "$LOCAL_FILE" 2>/dev/null; then
162
- echo "$line" >> "$LOCAL_FILE"
163
- MERGED=$((MERGED + 1))
164
- fi
165
- done < "$f"
166
- if [ "$MERGED" -gt 0 ]; then
167
- echo "Merged $MERGED new lines into $BASENAME"
168
- else
169
- echo "No new learnings to merge for $BASENAME"
170
- fi
171
- fi
172
- done
173
- else
174
- echo "No shared learnings found — continuing"
175
- fi
176
-
177
- rm -rf "$TMPDIR"
109
+ bunx @benzotti/jedi@latest action fetch-learnings --learnings-repo "$LEARNINGS_REPO"
178
110
  env:
179
111
  LEARNINGS_REPO: ${{ vars.JEDI_LEARNINGS_REPO }}
180
112
  LEARNINGS_TOKEN: ${{ secrets.JEDI_LEARNINGS_TOKEN || github.token }}
@@ -198,7 +130,7 @@ jobs:
198
130
  # Set JEDI_ALLOWED_USERS to a comma-separated list for explicit allow-listing.
199
131
  - name: Run Jedi
200
132
  run: |
201
- bunx @benzotti/jedi@latest action "$COMMENT_BODY" \
133
+ bunx @benzotti/jedi@latest action run "$COMMENT_BODY" \
202
134
  --repo "$REPO" \
203
135
  ${COMMENT_ID:+--comment-id "$COMMENT_ID"} \
204
136
  ${PR_NUMBER:+--pr-number "$PR_NUMBER"} \
@@ -241,32 +173,10 @@ jobs:
241
173
  - name: Check if Jedi was involved
242
174
  id: check
243
175
  run: |
244
- # Find the PR associated with this merge commit
245
- PR_NUMBER=$(gh api "repos/$REPO/commits/${{ github.sha }}/pulls" --jq '.[0].number // empty' 2>/dev/null || true)
246
- if [ -z "$PR_NUMBER" ]; then
247
- echo "No associated PR found — skipping"
248
- echo "skip=true" >> "$GITHUB_OUTPUT"
249
- exit 0
250
- fi
251
-
252
- # Check if Jedi commented on this PR (comments from github-actions bot mentioning Jedi)
253
- JEDI_ACTIVITY=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate \
254
- --jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | test("jedi|Jedi|🤖")))] | length' 2>/dev/null || echo "0")
255
-
256
- # Also check if jedi[bot] made any commits on the PR
257
- JEDI_COMMITS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/commits" --paginate \
258
- --jq '[.[] | select(.commit.author.name == "jedi[bot]")] | length' 2>/dev/null || echo "0")
259
-
260
- if [ "$JEDI_ACTIVITY" -gt 0 ] || [ "$JEDI_COMMITS" -gt 0 ]; then
261
- BRANCH=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '.head.ref')
262
- echo "Jedi was active on PR #$PR_NUMBER (comments: $JEDI_ACTIVITY, commits: $JEDI_COMMITS) — promoting learnings"
263
- echo "skip=false" >> "$GITHUB_OUTPUT"
264
- echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
265
- echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
266
- else
267
- echo "No Jedi activity on PR #$PR_NUMBER — skipping"
268
- echo "skip=true" >> "$GITHUB_OUTPUT"
269
- fi
176
+ bunx @benzotti/jedi@latest action promote-learnings \
177
+ --check-only \
178
+ --repo "$REPO" \
179
+ --sha "${{ github.sha }}"
270
180
  env:
271
181
  GH_TOKEN: ${{ github.token }}
272
182
  REPO: ${{ github.repository }}
@@ -315,98 +225,14 @@ jobs:
315
225
  && steps.restore.outputs.cache-hit != ''
316
226
  && vars.JEDI_COMMIT_LEARNINGS == 'true'
317
227
  run: |
318
- LEARNINGS_SRC=".jdi/framework/learnings"
319
- REMOTE_SUBDIR="jedi/learnings"
320
-
321
- # Only proceed if there are learnings files with content
322
- if [ ! -d "$LEARNINGS_SRC" ]; then
323
- echo "No learnings directory found — skipping commit"
324
- exit 0
325
- fi
326
-
327
- # Check if any learnings files have meaningful content (not just headers)
328
- HAS_CONTENT=false
329
- for f in "$LEARNINGS_SRC"/*.md; do
330
- [ -f "$f" ] || continue
331
- if grep -qvE '^\s*(#|<!--|$)' "$f" 2>/dev/null; then
332
- HAS_CONTENT=true
333
- break
334
- fi
335
- done
336
-
337
- if [ "$HAS_CONTENT" != "true" ]; then
338
- echo "No learnings content to commit — skipping"
339
- exit 0
340
- fi
341
-
342
228
  git config user.name "jedi[bot]"
343
229
  git config user.email "jedi[bot]@users.noreply.github.com"
344
-
345
- if [ -n "$LEARNINGS_REPO" ]; then
346
- # ── External repo: clone, merge learnings, push ──
347
- TMPDIR=$(mktemp -d)
348
-
349
- git clone --depth 1 \
350
- "https://x-access-token:${LEARNINGS_TOKEN}@github.com/${LEARNINGS_REPO}.git" \
351
- "$TMPDIR" 2>/dev/null || {
352
- echo "::warning::Could not clone learnings repo ${LEARNINGS_REPO} — skipping commit"
353
- exit 0
354
- }
355
-
356
- mkdir -p "$TMPDIR/$REMOTE_SUBDIR"
357
-
358
- # Merge learnings: append new lines from local that don't exist in remote
359
- for f in "$LEARNINGS_SRC"/*.md; do
360
- [ -f "$f" ] || continue
361
- BASENAME=$(basename "$f")
362
- REMOTE_FILE="$TMPDIR/$REMOTE_SUBDIR/$BASENAME"
363
-
364
- if [ -f "$REMOTE_FILE" ]; then
365
- # Append lines from local that aren't already in the remote file
366
- while IFS= read -r line; do
367
- [ -z "$line" ] && continue
368
- if ! grep -qFx "$line" "$REMOTE_FILE" 2>/dev/null; then
369
- echo "$line" >> "$REMOTE_FILE"
370
- fi
371
- done < "$f"
372
- echo "Merged learning: $BASENAME"
373
- else
374
- cp "$f" "$REMOTE_FILE"
375
- echo "Added new learning: $BASENAME"
376
- fi
377
- done
378
-
379
- cd "$TMPDIR"
380
- git config user.name "jedi[bot]"
381
- git config user.email "jedi[bot]@users.noreply.github.com"
382
- git add "$REMOTE_SUBDIR"/*.md
383
-
384
- if git diff --cached --quiet; then
385
- echo "Learnings unchanged in external repo — nothing to commit"
386
- else
387
- git commit -m "chore(jedi): update learnings from ${{ github.repository }}
388
-
389
- Source: PR #${{ steps.check.outputs.pr_number }} on ${{ github.repository }}
390
- Learnings accumulated from PR reviews and feedback."
391
- git push
392
- echo "Learnings committed to ${LEARNINGS_REPO}/$REMOTE_SUBDIR"
393
- fi
394
-
395
- cd - > /dev/null
396
- rm -rf "$TMPDIR"
397
- else
398
- # ── Same repo: commit directly to main ──
399
- git add "$LEARNINGS_SRC"/*.md
400
- if git diff --cached --quiet; then
401
- echo "Learnings unchanged — nothing to commit"
402
- else
403
- git commit -m "chore(jedi): update team learnings
404
-
405
- Auto-committed by Jedi after PR #${{ steps.check.outputs.pr_number }} merged.
406
- These learnings are accumulated from PR reviews and feedback."
407
- git push
408
- fi
409
- fi
230
+ bunx @benzotti/jedi@latest action promote-learnings \
231
+ --repo "$REPO" \
232
+ --sha "${{ github.sha }}" \
233
+ --pr-number "${{ steps.check.outputs.pr_number }}" \
234
+ --branch "${{ steps.check.outputs.branch }}" \
235
+ ${LEARNINGS_REPO:+--learnings-repo "$LEARNINGS_REPO"}
410
236
  env:
411
237
  LEARNINGS_REPO: ${{ vars.JEDI_LEARNINGS_REPO || '' }}
412
238
  LEARNINGS_TOKEN: ${{ secrets.JEDI_LEARNINGS_TOKEN || github.token }}
package/dist/index.js CHANGED
@@ -11292,7 +11292,7 @@ var planApproveCommand = defineCommand({
11292
11292
  }
11293
11293
  });
11294
11294
 
11295
- // src/commands/action.ts
11295
+ // src/commands/action/run.ts
11296
11296
  import { resolve as resolve11 } from "path";
11297
11297
 
11298
11298
  // src/utils/clickup.ts
@@ -11568,7 +11568,7 @@ function formatErrorComment(command, summary) {
11568
11568
  `);
11569
11569
  }
11570
11570
 
11571
- // src/commands/action.ts
11571
+ // src/commands/action/run.ts
11572
11572
  function parseComment(comment, isFollowUp) {
11573
11573
  const hasDryRun = /--dry-run/i.test(comment);
11574
11574
  const cleanComment = comment.replace(/--dry-run/gi, "").trim();
@@ -11625,9 +11625,9 @@ function parseComment(comment, isFollowUp) {
11625
11625
  }
11626
11626
  return { ...base, command: "plan", description };
11627
11627
  }
11628
- var actionCommand = defineCommand({
11628
+ var runCommand2 = defineCommand({
11629
11629
  meta: {
11630
- name: "action",
11630
+ name: "run",
11631
11631
  description: "GitHub Action entry point \u2014 parse 'Hey Jedi' comment and run workflow"
11632
11632
  },
11633
11633
  args: {
@@ -12129,9 +12129,506 @@ Use the ClickUp ticket above as the primary requirements source.` : ``,
12129
12129
  }
12130
12130
  });
12131
12131
 
12132
+ // src/commands/action/resolve-branch.ts
12133
+ import { appendFileSync } from "fs";
12134
+ function writeGitHubOutput(key, value) {
12135
+ const outputFile = process.env.GITHUB_OUTPUT;
12136
+ if (outputFile) {
12137
+ appendFileSync(outputFile, `${key}=${value}
12138
+ `);
12139
+ }
12140
+ console.log(`${key}=${value}`);
12141
+ }
12142
+ function resolveBranch(opts) {
12143
+ if (opts.prHeadRef) {
12144
+ return opts.prHeadRef;
12145
+ }
12146
+ if (opts.prNumber && opts.repo) {
12147
+ const result = Bun.spawnSync(["gh", "api", `repos/${opts.repo}/pulls/${opts.prNumber}`, "--jq", ".head.ref"], { stdout: "pipe", stderr: "pipe" });
12148
+ if (result.exitCode === 0) {
12149
+ const branch = result.stdout.toString().trim();
12150
+ if (branch)
12151
+ return branch;
12152
+ } else {
12153
+ consola.warn(`Failed to resolve branch via API: ${result.stderr.toString().trim()}`);
12154
+ }
12155
+ }
12156
+ return null;
12157
+ }
12158
+ var resolveBranchCommand = defineCommand({
12159
+ meta: {
12160
+ name: "resolve-branch",
12161
+ description: "Resolve the PR head branch for checkout"
12162
+ },
12163
+ args: {
12164
+ "pr-head-ref": {
12165
+ type: "string",
12166
+ description: "The PR head ref (if available from event context)"
12167
+ },
12168
+ "pr-number": {
12169
+ type: "string",
12170
+ description: "The PR number to look up"
12171
+ },
12172
+ repo: {
12173
+ type: "string",
12174
+ description: "Repository in owner/repo format",
12175
+ required: true
12176
+ }
12177
+ },
12178
+ run({ args }) {
12179
+ const branch = resolveBranch({
12180
+ prHeadRef: args["pr-head-ref"],
12181
+ prNumber: args["pr-number"],
12182
+ repo: args.repo
12183
+ });
12184
+ if (branch) {
12185
+ writeGitHubOutput("branch", branch);
12186
+ } else {
12187
+ consola.info("No branch resolved \u2014 no PR context available");
12188
+ }
12189
+ }
12190
+ });
12191
+
12192
+ // src/commands/action/bootstrap.ts
12193
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync, writeFileSync, rmSync, readdirSync } from "fs";
12194
+ import { join as join13 } from "path";
12195
+ function ensureFramework(cwd) {
12196
+ const frameworkDir = join13(cwd, ".jdi/framework");
12197
+ if (!existsSync14(frameworkDir)) {
12198
+ consola.info("Framework not found \u2014 initializing...");
12199
+ const result = Bun.spawnSync(["bunx", "@benzotti/jedi@latest", "init", "--ci"], {
12200
+ cwd,
12201
+ stdout: "inherit",
12202
+ stderr: "inherit"
12203
+ });
12204
+ if (result.exitCode !== 0) {
12205
+ consola.error("Failed to initialize framework");
12206
+ process.exit(1);
12207
+ }
12208
+ }
12209
+ mkdirSync4(join13(cwd, ".jdi/persistence"), { recursive: true });
12210
+ }
12211
+ function clearStaleState(cwd) {
12212
+ const plansDir = join13(cwd, ".jdi/plans");
12213
+ if (existsSync14(plansDir)) {
12214
+ for (const entry of readdirSync(plansDir)) {
12215
+ rmSync(join13(plansDir, entry), { recursive: true, force: true });
12216
+ }
12217
+ } else {
12218
+ mkdirSync4(plansDir, { recursive: true });
12219
+ }
12220
+ const configDir = join13(cwd, ".jdi/config");
12221
+ mkdirSync4(configDir, { recursive: true });
12222
+ writeFileSync(join13(configDir, "state.yaml"), `active_plan: null
12223
+ current_wave: null
12224
+ mode: null
12225
+ `);
12226
+ consola.info("Cache miss or fallback \u2014 cleared plan state");
12227
+ }
12228
+ function setupGitExclude(cwd) {
12229
+ const excludeDir = join13(cwd, ".git/info");
12230
+ mkdirSync4(excludeDir, { recursive: true });
12231
+ const excludePath = join13(excludeDir, "exclude");
12232
+ let content = "";
12233
+ if (existsSync14(excludePath)) {
12234
+ content = readFileSync(excludePath, "utf-8");
12235
+ }
12236
+ const patterns = [".jdi/", ".claude/"];
12237
+ for (const pattern of patterns) {
12238
+ const lines = content.split(`
12239
+ `);
12240
+ if (!lines.some((line) => line === pattern)) {
12241
+ content = content.endsWith(`
12242
+ `) || content === "" ? content : content + `
12243
+ `;
12244
+ content += pattern + `
12245
+ `;
12246
+ }
12247
+ }
12248
+ writeFileSync(excludePath, content);
12249
+ }
12250
+ var bootstrapCommand = defineCommand({
12251
+ meta: {
12252
+ name: "bootstrap",
12253
+ description: "Bootstrap Jedi framework, clear stale state, and configure git excludes"
12254
+ },
12255
+ args: {
12256
+ "cache-hit": {
12257
+ type: "string",
12258
+ description: "Cache hit status from actions/cache ('true' if exact match)"
12259
+ }
12260
+ },
12261
+ run({ args }) {
12262
+ const cwd = process.cwd();
12263
+ ensureFramework(cwd);
12264
+ if (args["cache-hit"] !== "true") {
12265
+ clearStaleState(cwd);
12266
+ }
12267
+ setupGitExclude(cwd);
12268
+ consola.success("Bootstrap complete");
12269
+ }
12270
+ });
12271
+
12272
+ // src/commands/action/fetch-learnings.ts
12273
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, copyFileSync, rmSync as rmSync2 } from "fs";
12274
+ import { join as join14 } from "path";
12275
+ import { mkdtempSync } from "fs";
12276
+ import { tmpdir } from "os";
12277
+ function cloneLearningsRepo(repo, token, tmpDir) {
12278
+ const cloneUrl = `https://x-access-token:${token}@github.com/${repo}.git`;
12279
+ const cloneResult = Bun.spawnSync(["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", cloneUrl, tmpDir], { stdout: "pipe", stderr: "pipe" });
12280
+ if (cloneResult.exitCode !== 0) {
12281
+ consola.warn("Could not clone learnings repo \u2014 continuing without shared learnings");
12282
+ return false;
12283
+ }
12284
+ Bun.spawnSync(["git", "sparse-checkout", "set", "jedi/learnings"], {
12285
+ cwd: tmpDir,
12286
+ stdout: "pipe",
12287
+ stderr: "pipe"
12288
+ });
12289
+ return existsSync15(join14(tmpDir, "jedi/learnings"));
12290
+ }
12291
+ function mergeLearnings(sourceDir, targetDir) {
12292
+ const result = { copied: 0, merged: 0 };
12293
+ if (!existsSync15(sourceDir)) {
12294
+ return result;
12295
+ }
12296
+ mkdirSync5(targetDir, { recursive: true });
12297
+ const files = readdirSync2(sourceDir).filter((f3) => f3.endsWith(".md"));
12298
+ for (const file of files) {
12299
+ const sourcePath = join14(sourceDir, file);
12300
+ const targetPath = join14(targetDir, file);
12301
+ if (!existsSync15(targetPath)) {
12302
+ copyFileSync(sourcePath, targetPath);
12303
+ result.copied++;
12304
+ consola.info(`Loaded shared learning: ${file}`);
12305
+ } else {
12306
+ const sourceContent = readFileSync2(sourcePath, "utf-8");
12307
+ const targetContent = readFileSync2(targetPath, "utf-8");
12308
+ const targetLines = new Set(targetContent.split(`
12309
+ `));
12310
+ const newLines = [];
12311
+ for (const line of sourceContent.split(`
12312
+ `)) {
12313
+ if (line.trim() === "")
12314
+ continue;
12315
+ if (!targetLines.has(line)) {
12316
+ newLines.push(line);
12317
+ }
12318
+ }
12319
+ if (newLines.length > 0) {
12320
+ const appendContent = (targetContent.endsWith(`
12321
+ `) ? "" : `
12322
+ `) + newLines.join(`
12323
+ `) + `
12324
+ `;
12325
+ writeFileSync2(targetPath, targetContent + appendContent);
12326
+ result.merged++;
12327
+ consola.info(`Merged ${newLines.length} new lines into ${file}`);
12328
+ } else {
12329
+ consola.info(`No new learnings to merge for ${file}`);
12330
+ }
12331
+ }
12332
+ }
12333
+ return result;
12334
+ }
12335
+ var fetchLearningsCommand = defineCommand({
12336
+ meta: {
12337
+ name: "fetch-learnings",
12338
+ description: "Fetch and merge shared learnings from an external repository"
12339
+ },
12340
+ args: {
12341
+ "learnings-repo": {
12342
+ type: "string",
12343
+ description: "External learnings repository (e.g. org/jedi-learnings)"
12344
+ },
12345
+ "learnings-token": {
12346
+ type: "string",
12347
+ description: "Token for accessing the learnings repo"
12348
+ }
12349
+ },
12350
+ run({ args }) {
12351
+ const learningsRepo = args["learnings-repo"];
12352
+ if (!learningsRepo) {
12353
+ consola.info("No learnings repo configured \u2014 skipping");
12354
+ return;
12355
+ }
12356
+ const token = args["learnings-token"] || process.env.LEARNINGS_TOKEN || process.env.GH_TOKEN || "";
12357
+ if (!token) {
12358
+ consola.warn("No token available for learnings repo \u2014 skipping");
12359
+ return;
12360
+ }
12361
+ const cwd = process.cwd();
12362
+ const learningsDir = join14(cwd, ".jdi/framework/learnings");
12363
+ mkdirSync5(learningsDir, { recursive: true });
12364
+ const tmpDir = mkdtempSync(join14(tmpdir(), "jedi-learnings-"));
12365
+ try {
12366
+ const cloned = cloneLearningsRepo(learningsRepo, token, tmpDir);
12367
+ if (!cloned) {
12368
+ return;
12369
+ }
12370
+ const sourceDir = join14(tmpDir, "jedi/learnings");
12371
+ const result = mergeLearnings(sourceDir, learningsDir);
12372
+ consola.success(`Learnings fetch complete (copied: ${result.copied}, merged: ${result.merged})`);
12373
+ } finally {
12374
+ rmSync2(tmpDir, { recursive: true, force: true });
12375
+ }
12376
+ }
12377
+ });
12378
+
12379
+ // src/commands/action/promote-learnings.ts
12380
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, appendFileSync as appendFileSync2 } from "fs";
12381
+ import { join as join15 } from "path";
12382
+ import { mkdtempSync as mkdtempSync2 } from "fs";
12383
+ import { tmpdir as tmpdir2 } from "os";
12384
+ function writeGitHubOutput2(key, value) {
12385
+ const outputFile = process.env.GITHUB_OUTPUT;
12386
+ if (outputFile) {
12387
+ appendFileSync2(outputFile, `${key}=${value}
12388
+ `);
12389
+ }
12390
+ console.log(`${key}=${value}`);
12391
+ }
12392
+ function checkJediInvolvement(repo, sha) {
12393
+ const prResult = Bun.spawnSync(["gh", "api", `repos/${repo}/commits/${sha}/pulls`, "--jq", ".[0].number // empty"], { stdout: "pipe", stderr: "pipe" });
12394
+ const prNumberStr = prResult.stdout.toString().trim();
12395
+ if (!prNumberStr) {
12396
+ consola.info("No associated PR found \u2014 skipping");
12397
+ return { skip: true };
12398
+ }
12399
+ const prNumber = parseInt(prNumberStr, 10);
12400
+ const commentsResult = Bun.spawnSync([
12401
+ "gh",
12402
+ "api",
12403
+ `repos/${repo}/issues/${prNumber}/comments`,
12404
+ "--paginate",
12405
+ "--jq",
12406
+ `[.[] | select(.user.login == "github-actions[bot]" and (.body | test("jedi|Jedi")))] | length`
12407
+ ], { stdout: "pipe", stderr: "pipe" });
12408
+ const jediActivity = parseInt(commentsResult.stdout.toString().trim() || "0", 10);
12409
+ const commitsResult = Bun.spawnSync([
12410
+ "gh",
12411
+ "api",
12412
+ `repos/${repo}/pulls/${prNumber}/commits`,
12413
+ "--paginate",
12414
+ "--jq",
12415
+ `[.[] | select(.commit.author.name == "jedi[bot]")] | length`
12416
+ ], { stdout: "pipe", stderr: "pipe" });
12417
+ const jediCommits = parseInt(commitsResult.stdout.toString().trim() || "0", 10);
12418
+ if (jediActivity > 0 || jediCommits > 0) {
12419
+ const branchResult = Bun.spawnSync(["gh", "api", `repos/${repo}/pulls/${prNumber}`, "--jq", ".head.ref"], { stdout: "pipe", stderr: "pipe" });
12420
+ const branch = branchResult.stdout.toString().trim();
12421
+ consola.info(`Jedi was active on PR #${prNumber} (comments: ${jediActivity}, commits: ${jediCommits}) \u2014 promoting learnings`);
12422
+ return { skip: false, branch, prNumber };
12423
+ }
12424
+ consola.info(`No Jedi activity on PR #${prNumber} \u2014 skipping`);
12425
+ return { skip: true };
12426
+ }
12427
+ function hasLearningsContent(learningsDir) {
12428
+ if (!existsSync16(learningsDir)) {
12429
+ return false;
12430
+ }
12431
+ const files = readdirSync3(learningsDir).filter((f3) => f3.endsWith(".md"));
12432
+ for (const file of files) {
12433
+ const content = readFileSync3(join15(learningsDir, file), "utf-8");
12434
+ const lines = content.split(`
12435
+ `);
12436
+ for (const line of lines) {
12437
+ const trimmed = line.trim();
12438
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("<!--")) {
12439
+ continue;
12440
+ }
12441
+ return true;
12442
+ }
12443
+ }
12444
+ return false;
12445
+ }
12446
+ function commitLearningsToSameRepo(learningsDir, prNumber) {
12447
+ const addResult = Bun.spawnSync(["git", "add", `${learningsDir}/*.md`], {
12448
+ stdout: "pipe",
12449
+ stderr: "pipe"
12450
+ });
12451
+ if (addResult.exitCode !== 0) {
12452
+ Bun.spawnSync(["bash", "-c", `git add "${learningsDir}"/*.md`], {
12453
+ stdout: "pipe",
12454
+ stderr: "pipe"
12455
+ });
12456
+ }
12457
+ const diffResult = Bun.spawnSync(["git", "diff", "--cached", "--quiet"], {
12458
+ stdout: "pipe",
12459
+ stderr: "pipe"
12460
+ });
12461
+ if (diffResult.exitCode === 0) {
12462
+ consola.info("Learnings unchanged \u2014 nothing to commit");
12463
+ return false;
12464
+ }
12465
+ const message = prNumber ? `chore(jedi): update team learnings
12466
+
12467
+ Auto-committed by Jedi after PR #${prNumber} merged.
12468
+ These learnings are accumulated from PR reviews and feedback.` : `chore(jedi): update team learnings`;
12469
+ const commitResult = Bun.spawnSync(["git", "commit", "-m", message], {
12470
+ stdout: "pipe",
12471
+ stderr: "pipe"
12472
+ });
12473
+ if (commitResult.exitCode !== 0) {
12474
+ consola.error("Failed to commit learnings:", commitResult.stderr.toString());
12475
+ return false;
12476
+ }
12477
+ const pushResult = Bun.spawnSync(["git", "push"], {
12478
+ stdout: "pipe",
12479
+ stderr: "pipe"
12480
+ });
12481
+ if (pushResult.exitCode !== 0) {
12482
+ consola.error("Failed to push learnings:", pushResult.stderr.toString());
12483
+ return false;
12484
+ }
12485
+ consola.success("Learnings committed and pushed");
12486
+ return true;
12487
+ }
12488
+ function commitLearningsToExternalRepo(learningsDir, externalRepo, token, prNumber, sourceRepo) {
12489
+ const tmpDir = mkdtempSync2(join15(tmpdir2(), "jedi-promote-"));
12490
+ try {
12491
+ const cloneUrl = `https://x-access-token:${token}@github.com/${externalRepo}.git`;
12492
+ const cloneResult = Bun.spawnSync(["git", "clone", "--depth", "1", cloneUrl, tmpDir], { stdout: "pipe", stderr: "pipe" });
12493
+ if (cloneResult.exitCode !== 0) {
12494
+ consola.warn(`Could not clone learnings repo ${externalRepo} \u2014 skipping commit`);
12495
+ return false;
12496
+ }
12497
+ const remoteSubdir = join15(tmpDir, "jedi/learnings");
12498
+ mkdirSync6(remoteSubdir, { recursive: true });
12499
+ mergeLearnings(learningsDir, remoteSubdir);
12500
+ Bun.spawnSync(["git", "config", "user.name", "jedi[bot]"], { cwd: tmpDir });
12501
+ Bun.spawnSync(["git", "config", "user.email", "jedi[bot]@users.noreply.github.com"], { cwd: tmpDir });
12502
+ Bun.spawnSync(["bash", "-c", `git add "jedi/learnings"/*.md`], {
12503
+ cwd: tmpDir,
12504
+ stdout: "pipe",
12505
+ stderr: "pipe"
12506
+ });
12507
+ const diffResult = Bun.spawnSync(["git", "diff", "--cached", "--quiet"], {
12508
+ cwd: tmpDir,
12509
+ stdout: "pipe",
12510
+ stderr: "pipe"
12511
+ });
12512
+ if (diffResult.exitCode === 0) {
12513
+ consola.info("Learnings unchanged in external repo \u2014 nothing to commit");
12514
+ return false;
12515
+ }
12516
+ const source = sourceRepo || "unknown";
12517
+ const prRef = prNumber ? `PR #${prNumber}` : "merge";
12518
+ const message = `chore(jedi): update learnings from ${source}
12519
+
12520
+ Source: ${prRef} on ${source}
12521
+ Learnings accumulated from PR reviews and feedback.`;
12522
+ const commitResult = Bun.spawnSync(["git", "commit", "-m", message], {
12523
+ cwd: tmpDir,
12524
+ stdout: "pipe",
12525
+ stderr: "pipe"
12526
+ });
12527
+ if (commitResult.exitCode !== 0) {
12528
+ consola.error("Failed to commit to external repo:", commitResult.stderr.toString());
12529
+ return false;
12530
+ }
12531
+ const pushResult = Bun.spawnSync(["git", "push"], {
12532
+ cwd: tmpDir,
12533
+ stdout: "pipe",
12534
+ stderr: "pipe"
12535
+ });
12536
+ if (pushResult.exitCode !== 0) {
12537
+ consola.error("Failed to push to external repo:", pushResult.stderr.toString());
12538
+ return false;
12539
+ }
12540
+ consola.success(`Learnings committed to ${externalRepo}/jedi/learnings`);
12541
+ return true;
12542
+ } finally {
12543
+ rmSync3(tmpDir, { recursive: true, force: true });
12544
+ }
12545
+ }
12546
+ var promoteLearningsCommand = defineCommand({
12547
+ meta: {
12548
+ name: "promote-learnings",
12549
+ description: "Check Jedi involvement and promote learnings after PR merge"
12550
+ },
12551
+ args: {
12552
+ repo: {
12553
+ type: "string",
12554
+ description: "Repository in owner/repo format",
12555
+ required: true
12556
+ },
12557
+ sha: {
12558
+ type: "string",
12559
+ description: "The merge commit SHA",
12560
+ required: true
12561
+ },
12562
+ "check-only": {
12563
+ type: "boolean",
12564
+ description: "Only check involvement, write outputs, and exit"
12565
+ },
12566
+ "pr-number": {
12567
+ type: "string",
12568
+ description: "PR number (if already known)"
12569
+ },
12570
+ branch: {
12571
+ type: "string",
12572
+ description: "Branch name (if already known)"
12573
+ },
12574
+ "learnings-repo": {
12575
+ type: "string",
12576
+ description: "External learnings repository"
12577
+ },
12578
+ "learnings-token": {
12579
+ type: "string",
12580
+ description: "Token for the learnings repo"
12581
+ }
12582
+ },
12583
+ run({ args }) {
12584
+ const repo = args.repo;
12585
+ const sha = args.sha;
12586
+ if (args["check-only"]) {
12587
+ const involvement = checkJediInvolvement(repo, sha);
12588
+ writeGitHubOutput2("skip", String(involvement.skip));
12589
+ if (involvement.branch) {
12590
+ writeGitHubOutput2("branch", involvement.branch);
12591
+ }
12592
+ if (involvement.prNumber) {
12593
+ writeGitHubOutput2("pr_number", String(involvement.prNumber));
12594
+ }
12595
+ return;
12596
+ }
12597
+ const cwd = process.cwd();
12598
+ const learningsDir = join15(cwd, ".jdi/framework/learnings");
12599
+ if (!hasLearningsContent(learningsDir)) {
12600
+ consola.info("No learnings content to commit \u2014 skipping");
12601
+ return;
12602
+ }
12603
+ const learningsRepo = args["learnings-repo"];
12604
+ const token = args["learnings-token"] || process.env.LEARNINGS_TOKEN || process.env.GH_TOKEN || "";
12605
+ const prNumber = args["pr-number"] ? parseInt(args["pr-number"], 10) : undefined;
12606
+ if (learningsRepo) {
12607
+ commitLearningsToExternalRepo(learningsDir, learningsRepo, token, prNumber, repo);
12608
+ } else {
12609
+ commitLearningsToSameRepo(learningsDir, prNumber);
12610
+ }
12611
+ }
12612
+ });
12613
+
12614
+ // src/commands/action/index.ts
12615
+ var actionCommand = defineCommand({
12616
+ meta: {
12617
+ name: "action",
12618
+ description: "GitHub Action commands \u2014 run workflows, bootstrap, manage learnings"
12619
+ },
12620
+ subCommands: {
12621
+ run: runCommand2,
12622
+ "resolve-branch": resolveBranchCommand,
12623
+ bootstrap: bootstrapCommand,
12624
+ "fetch-learnings": fetchLearningsCommand,
12625
+ "promote-learnings": promoteLearningsCommand
12626
+ }
12627
+ });
12628
+
12132
12629
  // src/commands/setup-action.ts
12133
- import { join as join13, dirname as dirname3 } from "path";
12134
- import { existsSync as existsSync14, mkdirSync as mkdirSync4 } from "fs";
12630
+ import { join as join16, dirname as dirname3 } from "path";
12631
+ import { existsSync as existsSync17, mkdirSync as mkdirSync7 } from "fs";
12135
12632
  var setupActionCommand = defineCommand({
12136
12633
  meta: {
12137
12634
  name: "setup-action",
@@ -12140,19 +12637,19 @@ var setupActionCommand = defineCommand({
12140
12637
  args: {},
12141
12638
  async run() {
12142
12639
  const cwd = process.cwd();
12143
- const workflowDest = join13(cwd, ".github", "workflows", "jedi.yml");
12144
- if (existsSync14(workflowDest)) {
12640
+ const workflowDest = join16(cwd, ".github", "workflows", "jedi.yml");
12641
+ if (existsSync17(workflowDest)) {
12145
12642
  consola.warn(`Workflow already exists at ${workflowDest}`);
12146
12643
  consola.info("Skipping workflow copy. Delete it manually to regenerate.");
12147
12644
  } else {
12148
- const templatePath = join13(import.meta.dir, "../action/workflow-template.yml");
12149
- if (!existsSync14(templatePath)) {
12645
+ const templatePath = join16(import.meta.dir, "../action/workflow-template.yml");
12646
+ if (!existsSync17(templatePath)) {
12150
12647
  consola.error("Workflow template not found. Ensure @benzotti/jedi is properly installed.");
12151
12648
  process.exit(1);
12152
12649
  }
12153
12650
  const dir = dirname3(workflowDest);
12154
- if (!existsSync14(dir))
12155
- mkdirSync4(dir, { recursive: true });
12651
+ if (!existsSync17(dir))
12652
+ mkdirSync7(dir, { recursive: true });
12156
12653
  const template = await Bun.file(templatePath).text();
12157
12654
  await Bun.write(workflowDest, template);
12158
12655
  consola.success(`Created ${workflowDest}`);
@@ -12294,7 +12791,7 @@ var stateCommand = defineCommand({
12294
12791
  // package.json
12295
12792
  var package_default = {
12296
12793
  name: "@benzotti/jedi",
12297
- version: "0.1.39",
12794
+ version: "0.1.40",
12298
12795
  description: "JDI - Context-efficient AI development framework for Claude Code",
12299
12796
  type: "module",
12300
12797
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benzotti/jedi",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "JDI - Context-efficient AI development framework for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {