@benzotti/jedi 0.1.33 → 0.1.35

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/README.md CHANGED
@@ -1,13 +1,6 @@
1
- ```
2
- ██╗███████╗██████╗ ██╗
3
- ██║██╔════╝██╔══██╗██║
4
- ██║█████╗ ██║ ██║██║
5
- ██ ██║██╔══╝ ██║ ██║██║
6
- ╚█████╔╝███████╗██████╔╝██║
7
- ╚════╝ ╚══════╝╚═════╝ ╚═╝
8
- ```
1
+ # Jedi
9
2
 
10
- **Context-efficient AI development framework for Claude Code.**
3
+ **Multi-agent development framework for Claude Code — plan, implement, review, and ship with specialist agents and minimal token overhead.**
11
4
 
12
5
  [![npm version](https://img.shields.io/npm/v/@benzotti/jedi)](https://www.npmjs.com/package/@benzotti/jedi) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
13
6
 
@@ -7,6 +7,9 @@
7
7
  #
8
8
  # Optional:
9
9
  # - Set CLICKUP_API_TOKEN secret for ClickUp ticket integration
10
+ # - Set JEDI_COMMIT_LEARNINGS=true repo variable to persist learnings in git
11
+ # - Set JEDI_LEARNINGS_REPO variable to push learnings to a central repo (e.g. org/jedi-learnings)
12
+ # - Set JEDI_LEARNINGS_TOKEN secret (PAT with repo access) when using an external learnings repo
10
13
  # - Run `npx @benzotti/jedi init` locally to customise framework files
11
14
  #
12
15
  # Usage: Comment on any issue or PR with "Hey Jedi" followed by a command:
@@ -21,7 +24,8 @@
21
24
  # reply with feedback to refine, or say "approved" to finalise.
22
25
  #
23
26
  # Learnings: Jedi accumulates team preferences across PRs automatically.
24
- # No files to commit — everything lives in the GitHub Actions cache.
27
+ # By default learnings live in the GitHub Actions cache. Set JEDI_COMMIT_LEARNINGS=true
28
+ # to persist them in git (recommended — cache entries expire after 7 days of inactivity).
25
29
 
26
30
  name: Jedi
27
31
 
@@ -117,6 +121,49 @@ jobs:
117
121
  grep -qxF "$pattern" .git/info/exclude 2>/dev/null || echo "$pattern" >> .git/info/exclude
118
122
  done
119
123
 
124
+ # Fetch shared learnings from an external repo if configured.
125
+ # This merges cross-project learnings into the local framework before agents run.
126
+ - name: Fetch shared learnings
127
+ if: vars.JEDI_LEARNINGS_REPO != ''
128
+ run: |
129
+ LEARNINGS_DIR=".jdi/framework/learnings"
130
+ mkdir -p "$LEARNINGS_DIR"
131
+
132
+ # Clone only the directory for this repo's learnings (sparse checkout)
133
+ REPO_SUBDIR="${{ github.repository }}"
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 "$REPO_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/$REPO_SUBDIR" ]; then
149
+ for f in "$TMPDIR/$REPO_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 for ${{ github.repository }} — continuing"
160
+ fi
161
+
162
+ rm -rf "$TMPDIR"
163
+ env:
164
+ LEARNINGS_REPO: ${{ vars.JEDI_LEARNINGS_REPO }}
165
+ LEARNINGS_TOKEN: ${{ secrets.JEDI_LEARNINGS_TOKEN || github.token }}
166
+
120
167
  # Configure git for Jedi commits (used when implementing changes on PR branches)
121
168
  - name: Configure git
122
169
  run: |
@@ -171,13 +218,49 @@ jobs:
171
218
  key: jedi-state-${{ github.repository }}-${{ steps.pr.outputs.branch || github.head_ref || github.ref_name }}-${{ github.run_id }}
172
219
 
173
220
  # ── Promote learnings to main baseline when PRs merge ──
221
+ # Only runs when Jedi was actually used on the merged PR
174
222
  promote-learnings:
175
223
  if: github.event_name == 'push'
176
224
  runs-on: ubuntu-latest
177
225
  steps:
226
+ - name: Check if Jedi was involved
227
+ id: check
228
+ 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
255
+ env:
256
+ GH_TOKEN: ${{ github.token }}
257
+ REPO: ${{ github.repository }}
258
+
178
259
  - uses: actions/checkout@v4
260
+ if: steps.check.outputs.skip != 'true'
179
261
 
180
- - name: Restore latest Jedi state
262
+ - name: Restore Jedi state from merged branch
263
+ if: steps.check.outputs.skip != 'true'
181
264
  id: restore
182
265
  uses: actions/cache@v4
183
266
  with:
@@ -189,10 +272,10 @@ jobs:
189
272
  .claude/
190
273
  key: jedi-state-${{ github.repository }}-main-promotion-${{ github.sha }}
191
274
  restore-keys: |
192
- jedi-state-${{ github.repository }}-
275
+ jedi-state-${{ github.repository }}-${{ steps.check.outputs.branch }}-
193
276
 
194
277
  - name: Promote to main baseline
195
- if: steps.restore.outputs.cache-hit != ''
278
+ if: steps.check.outputs.skip != 'true' && steps.restore.outputs.cache-hit != ''
196
279
  uses: actions/cache/save@v4
197
280
  with:
198
281
  path: |
@@ -202,3 +285,93 @@ jobs:
202
285
  .jdi/plans/
203
286
  .claude/
204
287
  key: jedi-state-${{ github.repository }}-main-${{ github.sha }}
288
+
289
+ # Opt-in: commit learnings so they survive cache eviction.
290
+ # Set repo variable JEDI_COMMIT_LEARNINGS=true to enable.
291
+ #
292
+ # Where learnings are committed:
293
+ # - Same repo (default): .jdi/framework/learnings/ on main
294
+ # - External repo: set JEDI_LEARNINGS_REPO (e.g. org/jedi-learnings)
295
+ # and JEDI_LEARNINGS_TOKEN secret. Learnings are namespaced under
296
+ # <owner>/<repo>/ in the external repo to support multiple projects.
297
+ - name: Commit learnings to repo
298
+ if: >-
299
+ steps.check.outputs.skip != 'true'
300
+ && steps.restore.outputs.cache-hit != ''
301
+ && vars.JEDI_COMMIT_LEARNINGS == 'true'
302
+ run: |
303
+ LEARNINGS_SRC=".jdi/framework/learnings"
304
+
305
+ # Only proceed if there are learnings files with content
306
+ if [ ! -d "$LEARNINGS_SRC" ]; then
307
+ echo "No learnings directory found — skipping commit"
308
+ exit 0
309
+ fi
310
+
311
+ # Check if any learnings files have meaningful content (not just headers)
312
+ HAS_CONTENT=false
313
+ for f in "$LEARNINGS_SRC"/*.md; do
314
+ [ -f "$f" ] || continue
315
+ if grep -qvE '^\s*(#|<!--|$)' "$f" 2>/dev/null; then
316
+ HAS_CONTENT=true
317
+ break
318
+ fi
319
+ done
320
+
321
+ if [ "$HAS_CONTENT" != "true" ]; then
322
+ echo "No learnings content to commit — skipping"
323
+ exit 0
324
+ fi
325
+
326
+ git config user.name "jedi[bot]"
327
+ git config user.email "jedi[bot]@users.noreply.github.com"
328
+
329
+ if [ -n "$LEARNINGS_REPO" ]; then
330
+ # ── External repo: clone, copy learnings, push ──
331
+ REPO_SUBDIR="${{ github.repository }}"
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/$REPO_SUBDIR"
342
+ cp "$LEARNINGS_SRC"/*.md "$TMPDIR/$REPO_SUBDIR/"
343
+
344
+ cd "$TMPDIR"
345
+ git config user.name "jedi[bot]"
346
+ git config user.email "jedi[bot]@users.noreply.github.com"
347
+ git add "$REPO_SUBDIR"/*.md
348
+
349
+ if git diff --cached --quiet; then
350
+ echo "Learnings unchanged in external repo — nothing to commit"
351
+ else
352
+ git commit -m "chore(jedi): update learnings from ${{ github.repository }}
353
+
354
+ Source: PR #${{ steps.check.outputs.pr_number }} on ${{ github.repository }}
355
+ Learnings accumulated from PR reviews and feedback."
356
+ git push
357
+ echo "Learnings committed to ${LEARNINGS_REPO}/${REPO_SUBDIR}"
358
+ fi
359
+
360
+ cd - > /dev/null
361
+ rm -rf "$TMPDIR"
362
+ else
363
+ # ── Same repo: commit directly to main ──
364
+ git add "$LEARNINGS_SRC"/*.md
365
+ if git diff --cached --quiet; then
366
+ echo "Learnings unchanged — nothing to commit"
367
+ else
368
+ git commit -m "chore(jedi): update team learnings
369
+
370
+ Auto-committed by Jedi after PR #${{ steps.check.outputs.pr_number }} merged.
371
+ These learnings are accumulated from PR reviews and feedback."
372
+ git push
373
+ fi
374
+ fi
375
+ env:
376
+ LEARNINGS_REPO: ${{ vars.JEDI_LEARNINGS_REPO || '' }}
377
+ LEARNINGS_TOKEN: ${{ secrets.JEDI_LEARNINGS_TOKEN || github.token }}
package/dist/index.js CHANGED
@@ -9748,6 +9748,13 @@ async function loadPersistedState(cwd, storage) {
9748
9748
  } else {
9749
9749
  learningsPath = dir;
9750
9750
  }
9751
+ if (!learningsPath && existsSync6(dir)) {
9752
+ const { readdirSync } = await import("fs");
9753
+ const files = readdirSync(dir).filter((f3) => f3.endsWith(".md"));
9754
+ if (files.length > 0) {
9755
+ learningsPath = dir;
9756
+ }
9757
+ }
9751
9758
  const codebaseIndex = await storage.load("codebase-index");
9752
9759
  if (codebaseIndex) {
9753
9760
  const cbDir = join6(cwd, ".jdi", "codebase");
@@ -10084,17 +10091,55 @@ async function writeState(cwd, state) {
10084
10091
  }
10085
10092
 
10086
10093
  // src/utils/state-handlers.ts
10094
+ var import_yaml4 = __toESM(require_dist(), 1);
10095
+ import { join as join9 } from "path";
10096
+ import { existsSync as existsSync9 } from "fs";
10097
+ function parseFrontmatter(content) {
10098
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
10099
+ if (!match)
10100
+ return null;
10101
+ try {
10102
+ return import_yaml4.parse(match[1]);
10103
+ } catch {
10104
+ return null;
10105
+ }
10106
+ }
10087
10107
  async function transitionToPlanReady(cwd, planPath, planName) {
10088
10108
  const state = await readState(cwd) ?? {};
10109
+ const fullPlanPath = planPath.startsWith("/") ? planPath : join9(cwd, planPath);
10110
+ let phase;
10111
+ let planNumber;
10112
+ let taskFiles = [];
10113
+ if (existsSync9(fullPlanPath)) {
10114
+ const content = await Bun.file(fullPlanPath).text();
10115
+ const fm = parseFrontmatter(content);
10116
+ if (fm) {
10117
+ if (fm.phase != null)
10118
+ phase = Number(fm.phase);
10119
+ if (fm.plan != null)
10120
+ planNumber = String(fm.plan);
10121
+ if (Array.isArray(fm.task_files))
10122
+ taskFiles = fm.task_files;
10123
+ }
10124
+ }
10089
10125
  state.position = {
10090
10126
  ...state.position,
10091
- plan: planPath,
10127
+ ...phase != null ? { phase } : {},
10128
+ plan: planNumber ?? planPath,
10092
10129
  plan_name: planName,
10093
10130
  status: "planning"
10094
10131
  };
10095
10132
  state.current_plan = {
10096
10133
  ...state.current_plan,
10097
- path: planPath
10134
+ path: planPath,
10135
+ tasks: taskFiles,
10136
+ completed_tasks: [],
10137
+ current_task_index: taskFiles.length > 0 ? 0 : null
10138
+ };
10139
+ state.progress = {
10140
+ ...state.progress,
10141
+ tasks_total: taskFiles.length,
10142
+ tasks_completed: 0
10098
10143
  };
10099
10144
  state.review = {
10100
10145
  ...state.review,
@@ -10142,9 +10187,10 @@ async function advanceTask(cwd, completedTaskId) {
10142
10187
  const nextIndex = completed.length;
10143
10188
  state.current_plan.current_task_index = nextIndex < tasks.length ? nextIndex : null;
10144
10189
  }
10145
- if (state.progress) {
10146
- state.progress.tasks_completed = (state.progress.tasks_completed ?? 0) + 1;
10190
+ if (!state.progress) {
10191
+ state.progress = { phases_total: 0, phases_completed: 0, plans_total: 0, plans_completed: 0, tasks_total: 0, tasks_completed: 0 };
10147
10192
  }
10193
+ state.progress.tasks_completed = (state.progress.tasks_completed ?? 0) + 1;
10148
10194
  await updateSessionActivity(cwd, state);
10149
10195
  }
10150
10196
  async function updateSessionActivity(cwd, state) {
@@ -10327,15 +10373,15 @@ var statusCommand = defineCommand({
10327
10373
  import { relative } from "path";
10328
10374
 
10329
10375
  // src/utils/resolve-components.ts
10330
- import { join as join9, basename } from "path";
10376
+ import { join as join10, basename } from "path";
10331
10377
  import { homedir } from "os";
10332
10378
  async function resolveComponents(cwd) {
10333
10379
  const components = [];
10334
10380
  const seen = new Set;
10335
10381
  const sources = [
10336
- { dir: join9(cwd, ".jdi", "framework", "components"), source: "project" },
10337
- { dir: join9(homedir(), ".jdi", "components"), source: "user" },
10338
- { dir: join9(import.meta.dir, "../framework/components"), source: "builtin" }
10382
+ { dir: join10(cwd, ".jdi", "framework", "components"), source: "project" },
10383
+ { dir: join10(homedir(), ".jdi", "components"), source: "user" },
10384
+ { dir: join10(import.meta.dir, "../framework/components"), source: "builtin" }
10339
10385
  ];
10340
10386
  for (const { dir, source } of sources) {
10341
10387
  try {
@@ -10344,7 +10390,7 @@ async function resolveComponents(cwd) {
10344
10390
  const name = basename(file, ".md");
10345
10391
  if (!seen.has(name)) {
10346
10392
  seen.add(name);
10347
- components.push({ name, path: join9(dir, file), source });
10393
+ components.push({ name, path: join10(dir, file), source });
10348
10394
  }
10349
10395
  }
10350
10396
  } catch {}
@@ -10490,8 +10536,8 @@ Use --all to stage and commit all, or stage files manually.`);
10490
10536
  });
10491
10537
 
10492
10538
  // src/commands/pr.ts
10493
- import { existsSync as existsSync9 } from "fs";
10494
- import { join as join10 } from "path";
10539
+ import { existsSync as existsSync10 } from "fs";
10540
+ import { join as join11 } from "path";
10495
10541
  async function hasGhCli() {
10496
10542
  const { exitCode } = await exec(["which", "gh"]);
10497
10543
  return exitCode === 0;
@@ -10542,8 +10588,8 @@ var prCommand = defineCommand({
10542
10588
  let verificationChecks = [];
10543
10589
  const planPath = state?.current_plan?.path;
10544
10590
  if (planPath) {
10545
- const fullPlanPath = join10(cwd, planPath);
10546
- if (existsSync9(fullPlanPath)) {
10591
+ const fullPlanPath = join11(cwd, planPath);
10592
+ if (existsSync10(fullPlanPath)) {
10547
10593
  try {
10548
10594
  const planContent = await Bun.file(fullPlanPath).text();
10549
10595
  const nameMatch = planContent.match(/^#\s+(.+)/m);
@@ -10566,8 +10612,8 @@ ${taskLines.map((l2) => `- ${l2.split("|").slice(2, 3).join("").trim()}`).join(`
10566
10612
  }
10567
10613
  }
10568
10614
  let template = "";
10569
- const templatePath = join10(cwd, ".github", "pull_request_template.md");
10570
- if (existsSync9(templatePath)) {
10615
+ const templatePath = join11(cwd, ".github", "pull_request_template.md");
10616
+ if (existsSync10(templatePath)) {
10571
10617
  template = await Bun.file(templatePath).text();
10572
10618
  }
10573
10619
  const title = branch.replace(/^(feat|fix|chore|docs|refactor|test|ci)\//, "").replace(/[-_]/g, " ").replace(/^\w/, (c3) => c3.toUpperCase());
@@ -10863,7 +10909,7 @@ var quickCommand = defineCommand({
10863
10909
  });
10864
10910
 
10865
10911
  // src/commands/worktree.ts
10866
- import { existsSync as existsSync10 } from "fs";
10912
+ import { existsSync as existsSync11 } from "fs";
10867
10913
  function slugify(name) {
10868
10914
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
10869
10915
  }
@@ -10897,7 +10943,7 @@ var worktreeCommand = defineCommand({
10897
10943
  }
10898
10944
  const slug = slugify(args.name);
10899
10945
  const worktreePath = `${root}/.worktrees/${slug}`;
10900
- if (existsSync10(worktreePath)) {
10946
+ if (existsSync11(worktreePath)) {
10901
10947
  consola.error(`Worktree already exists at ${worktreePath}`);
10902
10948
  return;
10903
10949
  }
@@ -11063,7 +11109,7 @@ Specify a worktree name: jdi worktree-remove <name>`);
11063
11109
 
11064
11110
  // src/commands/plan-review.ts
11065
11111
  import { resolve as resolve9 } from "path";
11066
- import { existsSync as existsSync11 } from "fs";
11112
+ import { existsSync as existsSync12 } from "fs";
11067
11113
  function parsePlanSummary(content) {
11068
11114
  const nameMatch = content.match(/^# .+?: (.+)$/m);
11069
11115
  const name = nameMatch?.[1] ?? "Unknown";
@@ -11102,7 +11148,7 @@ var planReviewCommand = defineCommand({
11102
11148
  consola.error("No plan found. Run `jdi plan` first.");
11103
11149
  return;
11104
11150
  }
11105
- if (!existsSync11(planPath)) {
11151
+ if (!existsSync12(planPath)) {
11106
11152
  consola.error(`Plan not found: ${planPath}`);
11107
11153
  return;
11108
11154
  }
@@ -11192,7 +11238,7 @@ Tasks (${tasks.length}):`);
11192
11238
 
11193
11239
  // src/commands/plan-approve.ts
11194
11240
  import { resolve as resolve10 } from "path";
11195
- import { existsSync as existsSync12 } from "fs";
11241
+ import { existsSync as existsSync13 } from "fs";
11196
11242
  var planApproveCommand = defineCommand({
11197
11243
  meta: {
11198
11244
  name: "plan-approve",
@@ -11221,7 +11267,7 @@ var planApproveCommand = defineCommand({
11221
11267
  consola.error("No plan to approve. Run `jdi plan` first.");
11222
11268
  return;
11223
11269
  }
11224
- if (!existsSync12(planPath)) {
11270
+ if (!existsSync13(planPath)) {
11225
11271
  consola.error(`Plan not found: ${planPath}`);
11226
11272
  return;
11227
11273
  }
@@ -11698,16 +11744,16 @@ Say **\`Hey Jedi implement\`** when you're ready to go.`;
11698
11744
  return;
11699
11745
  }
11700
11746
  if (intent.command === "ping") {
11701
- const { existsSync: existsSync13 } = await import("fs");
11702
- const { join: join12 } = await import("path");
11703
- const frameworkExists = existsSync13(join12(cwd, ".jdi/framework"));
11704
- const claudeMdExists = existsSync13(join12(cwd, ".claude/CLAUDE.md"));
11705
- const stateExists = existsSync13(join12(cwd, ".jdi/config/state.yaml"));
11706
- const learningsExists = existsSync13(join12(cwd, ".jdi/persistence/learnings.md"));
11747
+ const { existsSync: existsSync14 } = await import("fs");
11748
+ const { join: join13 } = await import("path");
11749
+ const frameworkExists = existsSync14(join13(cwd, ".jdi/framework"));
11750
+ const claudeMdExists = existsSync14(join13(cwd, ".claude/CLAUDE.md"));
11751
+ const stateExists = existsSync14(join13(cwd, ".jdi/config/state.yaml"));
11752
+ const learningsExists = existsSync14(join13(cwd, ".jdi/persistence/learnings.md"));
11707
11753
  let version = "unknown";
11708
11754
  try {
11709
- const pkgPath = join12(cwd, "node_modules/@benzotti/jedi/package.json");
11710
- if (existsSync13(pkgPath)) {
11755
+ const pkgPath = join13(cwd, "node_modules/@benzotti/jedi/package.json");
11756
+ if (existsSync14(pkgPath)) {
11711
11757
  const pkg = JSON.parse(await Bun.file(pkgPath).text());
11712
11758
  version = pkg.version;
11713
11759
  }
@@ -11823,11 +11869,12 @@ Say **\`Hey Jedi implement\`** when you're ready to go.`;
11823
11869
  `## Instructions`,
11824
11870
  `Read state.yaml and existing plan files. Apply feedback incrementally \u2014 do not restart from scratch.`,
11825
11871
  `If the feedback is a question, answer it conversationally. If it implies a plan change, update the plan.`,
11872
+ `Maintain the SPLIT plan format: update the index file and individual task files (.T{n}.md) separately.`,
11826
11873
  ``,
11827
11874
  `## Response Format (MANDATORY)`,
11828
- `1-2 sentence summary of what changed. Then the full updated plan in a collapsible block:`,
11875
+ `1-2 sentence summary of what changed. Then the updated plan summary in a collapsible block:`,
11829
11876
  `\`<details><summary>View full plan</summary> ... </details>\``,
11830
- `Use the same plan structure as the initial plan (tasks table, per-task details, verification).`,
11877
+ `Show the tasks manifest table and brief summaries \u2014 full task details are in the task files.`,
11831
11878
  `End with: "Any changes before implementation?"`
11832
11879
  ].join(`
11833
11880
  `);
@@ -11860,8 +11907,14 @@ Use the ClickUp ticket above as the primary requirements source.` : ``,
11860
11907
  `## Learnings`,
11861
11908
  `Before planning, read .jdi/persistence/learnings.md and .jdi/framework/learnings/ if they exist. Apply any team preferences found.`,
11862
11909
  ``,
11910
+ `## Plan File Format`,
11911
+ `CRITICAL: You MUST write plan files in SPLIT format as defined in your spec (Step 7a):`,
11912
+ `1. Write the index file: \`.jdi/plans/{phase}-{plan}-{slug}.plan.md\` \u2014 contains frontmatter with \`task_files:\` list and a manifest table only (NO inline task details)`,
11913
+ `2. Write each task as a separate file: \`.jdi/plans/{phase}-{plan}-{slug}.T{n}.md\` \u2014 one file per task with full implementation details`,
11914
+ `This split format is MANDATORY \u2014 it reduces token usage by letting agents load only their assigned task.`,
11915
+ ``,
11863
11916
  `## Response Format`,
11864
- `Follow the planning workflow in your spec to write plan files. Then respond with EXACTLY this structure (no deviations, no meta-commentary like "You are now active as..." or "Plan created"):`,
11917
+ `After writing the split plan files, respond with EXACTLY this structure (no deviations, no meta-commentary):`,
11865
11918
  ``,
11866
11919
  `1-2 sentence summary of the approach.`,
11867
11920
  ``,
@@ -11878,16 +11931,7 @@ Use the ClickUp ticket above as the primary requirements source.` : ``,
11878
11931
  `|------|------|------|------|------|`,
11879
11932
  `| T1 | {name} | {S|M|L} | auto | 1 |`,
11880
11933
  ``,
11881
- `### T1 \u2014 {Task Name}`,
11882
- `**Objective:** {what this achieves}`,
11883
- ``,
11884
- `**Steps:**`,
11885
- `1. {step}`,
11886
- ``,
11887
- `**Done when:** {completion criterion}`,
11888
- ``,
11889
- `---`,
11890
- `(repeat for each task)`,
11934
+ `(For each task, show a brief 1-2 line summary \u2014 full details are in the task files)`,
11891
11935
  ``,
11892
11936
  `### Verification`,
11893
11937
  `- [ ] {check 1}`,
@@ -12086,8 +12130,8 @@ Use the ClickUp ticket above as the primary requirements source.` : ``,
12086
12130
  });
12087
12131
 
12088
12132
  // src/commands/setup-action.ts
12089
- import { join as join12, dirname as dirname3 } from "path";
12090
- import { existsSync as existsSync13, mkdirSync as mkdirSync4 } from "fs";
12133
+ import { join as join13, dirname as dirname3 } from "path";
12134
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4 } from "fs";
12091
12135
  var setupActionCommand = defineCommand({
12092
12136
  meta: {
12093
12137
  name: "setup-action",
@@ -12096,18 +12140,18 @@ var setupActionCommand = defineCommand({
12096
12140
  args: {},
12097
12141
  async run() {
12098
12142
  const cwd = process.cwd();
12099
- const workflowDest = join12(cwd, ".github", "workflows", "jedi.yml");
12100
- if (existsSync13(workflowDest)) {
12143
+ const workflowDest = join13(cwd, ".github", "workflows", "jedi.yml");
12144
+ if (existsSync14(workflowDest)) {
12101
12145
  consola.warn(`Workflow already exists at ${workflowDest}`);
12102
12146
  consola.info("Skipping workflow copy. Delete it manually to regenerate.");
12103
12147
  } else {
12104
- const templatePath = join12(import.meta.dir, "../action/workflow-template.yml");
12105
- if (!existsSync13(templatePath)) {
12148
+ const templatePath = join13(import.meta.dir, "../action/workflow-template.yml");
12149
+ if (!existsSync14(templatePath)) {
12106
12150
  consola.error("Workflow template not found. Ensure @benzotti/jedi is properly installed.");
12107
12151
  process.exit(1);
12108
12152
  }
12109
12153
  const dir = dirname3(workflowDest);
12110
- if (!existsSync13(dir))
12154
+ if (!existsSync14(dir))
12111
12155
  mkdirSync4(dir, { recursive: true });
12112
12156
  const template = await Bun.file(templatePath).text();
12113
12157
  await Bun.write(workflowDest, template);
@@ -12250,7 +12294,7 @@ var stateCommand = defineCommand({
12250
12294
  // package.json
12251
12295
  var package_default = {
12252
12296
  name: "@benzotti/jedi",
12253
- version: "0.1.33",
12297
+ version: "0.1.35",
12254
12298
  description: "JDI - Context-efficient AI development framework for Claude Code",
12255
12299
  type: "module",
12256
12300
  bin: {
@@ -37,13 +37,17 @@ Before planning, ALWAYS:
37
37
 
38
38
  You MUST write files using Write/Edit tools. Returning plan content as text is NOT acceptable.
39
39
 
40
- Required files:
41
- 1. `.jdi/plans/{phase}-{plan}-{slug}.plan.md` (index file)
42
- 2. `.jdi/plans/{phase}-{plan}-{slug}.T{n}.md` (one per task)
40
+ **SANDBOX OVERRIDE:** Plan file creation is an EXCEPTION to AgentBase sandbox rules. You MUST use the Write tool to create plan files directly — do NOT defer them to `files_to_create`. Plan files WILL persist when written via the Write tool. This override applies ONLY to `.jdi/plans/` and `.jdi/config/` files listed below.
41
+
42
+ Required files (SPLIT FORMAT — one file per task):
43
+ 1. `.jdi/plans/{phase}-{plan}-{slug}.plan.md` (index file — manifest table only, NO inline task details)
44
+ 2. `.jdi/plans/{phase}-{plan}-{slug}.T{n}.md` (one per task — full implementation details)
43
45
  3. `.jdi/config/variables.yaml`
44
46
  4. `.jdi/ROADMAP.yaml` (add plan entry)
45
47
  5. `.jdi/REQUIREMENTS.yaml` (add traceability)
46
48
 
49
+ The split format is MANDATORY. Each task MUST be a separate `.T{n}.md` file. The index file contains ONLY the frontmatter (with `task_files:` list) and a manifest table — NEVER inline task implementation details.
50
+
47
51
  **Do NOT manually edit `.jdi/config/state.yaml`** — state transitions are handled via CLI commands (e.g. `npx jdi state plan-ready`).
48
52
 
49
53
  ## File Naming
@@ -20,8 +20,8 @@ Create an implementation plan using a single planner agent (includes research).
20
20
  2. Read codebase context (`.jdi/codebase/SUMMARY.md` if exists)
21
21
  3. Read scaffolding (.jdi/PROJECT.yaml, REQUIREMENTS.yaml, ROADMAP.yaml) — create from templates if missing
22
22
  4. Quick Mode Detection — suggest /jdi:quick for trivial tasks
23
- 5. Spawn `jdi-planner` agent (subagent_type="general-purpose") — creates PLAN.md with tasks, deps, waves
24
- 6. Collect and execute deferred ops
23
+ 5. Spawn `jdi-planner` agent (subagent_type="general-purpose") — creates split plan files (index .plan.md + per-task .T{n}.md files). The planner MUST write these files directly via Write tool (sandbox override for plan files).
24
+ 6. Collect and execute deferred ops — if the agent returned `files_to_create`, create them now using Write tool. Verify split plan files exist: index `.plan.md` + individual `.T{n}.md` task files.
25
25
  7. **Update state via CLI** — do NOT manually edit state.yaml. Run:
26
26
  ```bash
27
27
  npx jdi state plan-ready --plan-path ".jdi/plans/{plan-file}" --plan-name "{plan name}"
@@ -54,3 +54,7 @@ Do NOT manually edit `.jdi/config/state.yaml` for status transitions. Use the CL
54
54
  - `npx jdi state advance-task {task-id}` — after each task completes
55
55
 
56
56
  You may only append to `decisions`, `deviations`, or `blockers` arrays in state.yaml directly via `<JDI:StateUpdate />`.
57
+
58
+ ## Self-Testing (Jedi development only)
59
+
60
+ If the current project is the Jedi framework itself (`@benzotti/jedi`), run `bun test` after modifying prompt builders, action commands, or framework files. This catches regressions in split format references, learnings inclusion, and framework invariants.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benzotti/jedi",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "JDI - Context-efficient AI development framework for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {