@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
|
-
**
|
|
3
|
+
**Multi-agent development framework for Claude Code — plan, implement, review, and ship with specialist agents and minimal token overhead.**
|
|
11
4
|
|
|
12
5
|
[](https://www.npmjs.com/package/@benzotti/jedi) [](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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
10337
|
-
{ dir:
|
|
10338
|
-
{ dir:
|
|
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:
|
|
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
|
|
10494
|
-
import { join as
|
|
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 =
|
|
10546
|
-
if (
|
|
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 =
|
|
10570
|
-
if (
|
|
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
|
|
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 (
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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:
|
|
11702
|
-
const { join:
|
|
11703
|
-
const frameworkExists =
|
|
11704
|
-
const claudeMdExists =
|
|
11705
|
-
const stateExists =
|
|
11706
|
-
const learningsExists =
|
|
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 =
|
|
11710
|
-
if (
|
|
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
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
|
12090
|
-
import { existsSync as
|
|
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 =
|
|
12100
|
-
if (
|
|
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 =
|
|
12105
|
-
if (!
|
|
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 (!
|
|
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.
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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.
|