@benzotti/jedi 0.1.38 → 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,66 +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
- # Copy learnings from external repo into local framework
147
- # External learnings serve as baseline; cache/local files take precedence
148
- if [ -d "$TMPDIR/$REMOTE_SUBDIR" ]; then
149
- for f in "$TMPDIR/$REMOTE_SUBDIR"/*.md; do
150
- [ -f "$f" ] || continue
151
- BASENAME=$(basename "$f")
152
- # Only copy if local file doesn't exist (cache/committed files take precedence)
153
- if [ ! -f "$LEARNINGS_DIR/$BASENAME" ]; then
154
- cp "$f" "$LEARNINGS_DIR/$BASENAME"
155
- echo "Loaded shared learning: $BASENAME"
156
- fi
157
- done
158
- else
159
- echo "No shared learnings found — continuing"
160
- fi
161
-
162
- rm -rf "$TMPDIR"
109
+ bunx @benzotti/jedi@latest action fetch-learnings --learnings-repo "$LEARNINGS_REPO"
163
110
  env:
164
111
  LEARNINGS_REPO: ${{ vars.JEDI_LEARNINGS_REPO }}
165
112
  LEARNINGS_TOKEN: ${{ secrets.JEDI_LEARNINGS_TOKEN || github.token }}
@@ -183,7 +130,7 @@ jobs:
183
130
  # Set JEDI_ALLOWED_USERS to a comma-separated list for explicit allow-listing.
184
131
  - name: Run Jedi
185
132
  run: |
186
- bunx @benzotti/jedi@latest action "$COMMENT_BODY" \
133
+ bunx @benzotti/jedi@latest action run "$COMMENT_BODY" \
187
134
  --repo "$REPO" \
188
135
  ${COMMENT_ID:+--comment-id "$COMMENT_ID"} \
189
136
  ${PR_NUMBER:+--pr-number "$PR_NUMBER"} \
@@ -226,32 +173,10 @@ jobs:
226
173
  - name: Check if Jedi was involved
227
174
  id: check
228
175
  run: |
229
- # Find the PR associated with this merge commit
230
- PR_NUMBER=$(gh api "repos/$REPO/commits/${{ github.sha }}/pulls" --jq '.[0].number // empty' 2>/dev/null || true)
231
- if [ -z "$PR_NUMBER" ]; then
232
- echo "No associated PR found — skipping"
233
- echo "skip=true" >> "$GITHUB_OUTPUT"
234
- exit 0
235
- fi
236
-
237
- # Check if Jedi commented on this PR (comments from github-actions bot mentioning Jedi)
238
- JEDI_ACTIVITY=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate \
239
- --jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | test("jedi|Jedi|🤖")))] | length' 2>/dev/null || echo "0")
240
-
241
- # Also check if jedi[bot] made any commits on the PR
242
- JEDI_COMMITS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/commits" --paginate \
243
- --jq '[.[] | select(.commit.author.name == "jedi[bot]")] | length' 2>/dev/null || echo "0")
244
-
245
- if [ "$JEDI_ACTIVITY" -gt 0 ] || [ "$JEDI_COMMITS" -gt 0 ]; then
246
- BRANCH=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '.head.ref')
247
- echo "Jedi was active on PR #$PR_NUMBER (comments: $JEDI_ACTIVITY, commits: $JEDI_COMMITS) — promoting learnings"
248
- echo "skip=false" >> "$GITHUB_OUTPUT"
249
- echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
250
- echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
251
- else
252
- echo "No Jedi activity on PR #$PR_NUMBER — skipping"
253
- echo "skip=true" >> "$GITHUB_OUTPUT"
254
- fi
176
+ bunx @benzotti/jedi@latest action promote-learnings \
177
+ --check-only \
178
+ --repo "$REPO" \
179
+ --sha "${{ github.sha }}"
255
180
  env:
256
181
  GH_TOKEN: ${{ github.token }}
257
182
  REPO: ${{ github.repository }}
@@ -300,98 +225,14 @@ jobs:
300
225
  && steps.restore.outputs.cache-hit != ''
301
226
  && vars.JEDI_COMMIT_LEARNINGS == 'true'
302
227
  run: |
303
- LEARNINGS_SRC=".jdi/framework/learnings"
304
- REMOTE_SUBDIR="jedi/learnings"
305
-
306
- # Only proceed if there are learnings files with content
307
- if [ ! -d "$LEARNINGS_SRC" ]; then
308
- echo "No learnings directory found — skipping commit"
309
- exit 0
310
- fi
311
-
312
- # Check if any learnings files have meaningful content (not just headers)
313
- HAS_CONTENT=false
314
- for f in "$LEARNINGS_SRC"/*.md; do
315
- [ -f "$f" ] || continue
316
- if grep -qvE '^\s*(#|<!--|$)' "$f" 2>/dev/null; then
317
- HAS_CONTENT=true
318
- break
319
- fi
320
- done
321
-
322
- if [ "$HAS_CONTENT" != "true" ]; then
323
- echo "No learnings content to commit — skipping"
324
- exit 0
325
- fi
326
-
327
228
  git config user.name "jedi[bot]"
328
229
  git config user.email "jedi[bot]@users.noreply.github.com"
329
-
330
- if [ -n "$LEARNINGS_REPO" ]; then
331
- # ── External repo: clone, merge learnings, push ──
332
- TMPDIR=$(mktemp -d)
333
-
334
- git clone --depth 1 \
335
- "https://x-access-token:${LEARNINGS_TOKEN}@github.com/${LEARNINGS_REPO}.git" \
336
- "$TMPDIR" 2>/dev/null || {
337
- echo "::warning::Could not clone learnings repo ${LEARNINGS_REPO} — skipping commit"
338
- exit 0
339
- }
340
-
341
- mkdir -p "$TMPDIR/$REMOTE_SUBDIR"
342
-
343
- # Merge learnings: append new lines from local that don't exist in remote
344
- for f in "$LEARNINGS_SRC"/*.md; do
345
- [ -f "$f" ] || continue
346
- BASENAME=$(basename "$f")
347
- REMOTE_FILE="$TMPDIR/$REMOTE_SUBDIR/$BASENAME"
348
-
349
- if [ -f "$REMOTE_FILE" ]; then
350
- # Append lines from local that aren't already in the remote file
351
- while IFS= read -r line; do
352
- [ -z "$line" ] && continue
353
- if ! grep -qFx "$line" "$REMOTE_FILE" 2>/dev/null; then
354
- echo "$line" >> "$REMOTE_FILE"
355
- fi
356
- done < "$f"
357
- echo "Merged learning: $BASENAME"
358
- else
359
- cp "$f" "$REMOTE_FILE"
360
- echo "Added new learning: $BASENAME"
361
- fi
362
- done
363
-
364
- cd "$TMPDIR"
365
- git config user.name "jedi[bot]"
366
- git config user.email "jedi[bot]@users.noreply.github.com"
367
- git add "$REMOTE_SUBDIR"/*.md
368
-
369
- if git diff --cached --quiet; then
370
- echo "Learnings unchanged in external repo — nothing to commit"
371
- else
372
- git commit -m "chore(jedi): update learnings from ${{ github.repository }}
373
-
374
- Source: PR #${{ steps.check.outputs.pr_number }} on ${{ github.repository }}
375
- Learnings accumulated from PR reviews and feedback."
376
- git push
377
- echo "Learnings committed to ${LEARNINGS_REPO}/$REMOTE_SUBDIR"
378
- fi
379
-
380
- cd - > /dev/null
381
- rm -rf "$TMPDIR"
382
- else
383
- # ── Same repo: commit directly to main ──
384
- git add "$LEARNINGS_SRC"/*.md
385
- if git diff --cached --quiet; then
386
- echo "Learnings unchanged — nothing to commit"
387
- else
388
- git commit -m "chore(jedi): update team learnings
389
-
390
- Auto-committed by Jedi after PR #${{ steps.check.outputs.pr_number }} merged.
391
- These learnings are accumulated from PR reviews and feedback."
392
- git push
393
- fi
394
- 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"}
395
236
  env:
396
237
  LEARNINGS_REPO: ${{ vars.JEDI_LEARNINGS_REPO || '' }}
397
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.38",
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.38",
3
+ "version": "0.1.40",
4
4
  "description": "JDI - Context-efficient AI development framework for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {