@jskit-ai/jskit-cli 0.2.80 → 0.2.81
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/package.json +4 -4
- package/src/server/commandHandlers/session.js +71 -3
- package/src/server/core/argParser.js +12 -0
- package/src/server/core/commandCatalog.js +14 -7
- package/src/server/sessionRuntime/constants.js +150 -126
- package/src/server/sessionRuntime/paths.js +2 -4
- package/src/server/sessionRuntime/preconditions.js +25 -35
- package/src/server/sessionRuntime/prompts/app_blueprint.md +26 -2
- package/src/server/sessionRuntime/prompts/doctor_failure.md +12 -1
- package/src/server/sessionRuntime/prompts/execute_plan.md +35 -0
- package/src/server/sessionRuntime/prompts/new_issue.md +21 -3
- package/src/server/sessionRuntime/prompts/plan_issue.md +50 -0
- package/src/server/sessionRuntime/prompts/pr_failure.md +14 -1
- package/src/server/sessionRuntime/prompts/review_changes.md +36 -15
- package/src/server/sessionRuntime/prompts/user_check.md +7 -3
- package/src/server/sessionRuntime/responses.js +146 -19
- package/src/server/sessionRuntime/worktrees.js +31 -0
- package/src/server/sessionRuntime.js +419 -128
- package/src/server/sessionRuntime/prompts/implement_issue.md +0 -25
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mkdir,
|
|
3
3
|
readFile,
|
|
4
|
-
readdir
|
|
4
|
+
readdir,
|
|
5
|
+
rmdir
|
|
5
6
|
} from "node:fs/promises";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import {
|
|
9
|
+
PLAN_EXECUTION_CODEX_HANDOFF,
|
|
10
|
+
REVIEW_EXECUTION_CODEX_HANDOFF,
|
|
8
11
|
SESSION_STATUS,
|
|
9
12
|
STEP_DEFINITIONS,
|
|
10
13
|
STEP_IDS,
|
|
@@ -47,8 +50,8 @@ import {
|
|
|
47
50
|
assertGitCurrentBranch,
|
|
48
51
|
assertGitRepository,
|
|
49
52
|
assertGithubOrigin,
|
|
50
|
-
assertIssueArtifacts,
|
|
51
53
|
assertIssueTextExists,
|
|
54
|
+
assertIssueUrlExists,
|
|
52
55
|
assertPrUrlExists,
|
|
53
56
|
assertSessionExists,
|
|
54
57
|
assertTargetRootWritable,
|
|
@@ -121,10 +124,31 @@ async function withExistingSession(input, handler) {
|
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
function
|
|
127
|
+
function extractMarkedText(value = "", marker = "") {
|
|
125
128
|
const text = normalizeText(value);
|
|
126
|
-
const
|
|
127
|
-
|
|
129
|
+
const normalizedMarker = normalizeText(marker);
|
|
130
|
+
if (!normalizedMarker) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
const pattern = new RegExp(`\\[${normalizedMarker}\\]([\\s\\S]*?)\\[/${normalizedMarker}\\]`, "u");
|
|
134
|
+
const match = pattern.exec(text);
|
|
135
|
+
return normalizeText(match ? match[1] : "");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractIssueTitle(value = "") {
|
|
139
|
+
return extractMarkedText(value, "issue_title");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractIssueText(value = "") {
|
|
143
|
+
return extractMarkedText(value, "issue_text") || normalizeText(value);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractPlanText(value = "") {
|
|
147
|
+
return extractMarkedText(value, "plan") || normalizeText(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function writePromptArtifact(paths, fileName, prompt) {
|
|
151
|
+
await writeTextFile(path.join(paths.sessionRoot, "prompts", fileName), prompt);
|
|
128
152
|
}
|
|
129
153
|
|
|
130
154
|
async function createSession({
|
|
@@ -161,7 +185,6 @@ async function createSession({
|
|
|
161
185
|
|
|
162
186
|
await ensureStudioGitExclude(initialPaths.targetRoot);
|
|
163
187
|
await mkdir(initialPaths.sessionRoot, { recursive: true });
|
|
164
|
-
await mkdir(initialPaths.worktreesRoot, { recursive: true });
|
|
165
188
|
await writeTextFile(path.join(initialPaths.sessionRoot, "transcript.log"), "");
|
|
166
189
|
await markStatus(initialPaths, SESSION_STATUS.PENDING);
|
|
167
190
|
await writeReceipt(initialPaths, "session_created", `Created JSKIT Studio issue session ${initialPaths.sessionId}.`);
|
|
@@ -172,14 +195,38 @@ async function createSession({
|
|
|
172
195
|
});
|
|
173
196
|
}
|
|
174
197
|
|
|
175
|
-
|
|
198
|
+
const SESSION_ARCHIVE_ROOTS = Object.freeze([
|
|
199
|
+
"active",
|
|
200
|
+
"completed",
|
|
201
|
+
"abandoned"
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
function normalizeArchiveFilter(archive = "active") {
|
|
205
|
+
const requestedArchives = Array.isArray(archive) ? archive : [archive];
|
|
206
|
+
const normalized = requestedArchives
|
|
207
|
+
.map((entry) => String(entry || "").trim().toLowerCase())
|
|
208
|
+
.filter(Boolean);
|
|
209
|
+
if (normalized.includes("all")) {
|
|
210
|
+
return SESSION_ARCHIVE_ROOTS;
|
|
211
|
+
}
|
|
212
|
+
const allowed = new Set(SESSION_ARCHIVE_ROOTS);
|
|
213
|
+
const selected = normalized.filter((entry) => allowed.has(entry));
|
|
214
|
+
return selected.length > 0 ? [...new Set(selected)] : ["active"];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function listSessions({ targetRoot = process.cwd(), archive = "active" } = {}) {
|
|
176
218
|
const paths = resolveSessionPaths({ targetRoot });
|
|
177
219
|
const sessions = [];
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
220
|
+
const rootsByArchive = {
|
|
221
|
+
abandoned: paths.abandonedSessionsRoot,
|
|
222
|
+
active: paths.sessionsRoot,
|
|
223
|
+
completed: paths.completedSessionsRoot
|
|
224
|
+
};
|
|
225
|
+
const selectedArchives = normalizeArchiveFilter(archive);
|
|
226
|
+
const roots = selectedArchives.map((archiveName) => ({
|
|
227
|
+
archive: archiveName,
|
|
228
|
+
root: rootsByArchive[archiveName]
|
|
229
|
+
}));
|
|
183
230
|
|
|
184
231
|
for (const rootInfo of roots) {
|
|
185
232
|
let entries = [];
|
|
@@ -207,6 +254,8 @@ async function listSessions({ targetRoot = process.cwd() } = {}) {
|
|
|
207
254
|
}
|
|
208
255
|
sessions.sort((left, right) => right.sessionId.localeCompare(left.sessionId));
|
|
209
256
|
return {
|
|
257
|
+
archive: selectedArchives.length === 1 ? selectedArchives[0] : "mixed",
|
|
258
|
+
archives: selectedArchives,
|
|
210
259
|
ok: true,
|
|
211
260
|
stepDefinitions: buildStepDefinitions(),
|
|
212
261
|
sessions
|
|
@@ -227,7 +276,9 @@ async function inspectSession({
|
|
|
227
276
|
function emptySessionDetails(response) {
|
|
228
277
|
return {
|
|
229
278
|
...response,
|
|
279
|
+
issueTitle: "",
|
|
230
280
|
issueText: "",
|
|
281
|
+
planText: "",
|
|
231
282
|
receipts: [],
|
|
232
283
|
transcriptLog: ""
|
|
233
284
|
};
|
|
@@ -244,20 +295,50 @@ async function inspectSessionDetails({
|
|
|
244
295
|
const { paths, preconditions } = context;
|
|
245
296
|
const response = await buildSessionResponse(paths, { preconditions });
|
|
246
297
|
|
|
247
|
-
const [issueText, receipts, transcriptLog] = await Promise.all([
|
|
298
|
+
const [issueText, issueTitle, planText, receipts, transcriptLog] = await Promise.all([
|
|
248
299
|
readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
|
|
300
|
+
readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
|
|
301
|
+
readTextIfExists(path.join(paths.sessionRoot, "plan.md")),
|
|
249
302
|
readReceiptSteps(paths),
|
|
250
303
|
readTextIfExists(path.join(paths.sessionRoot, "transcript.log"))
|
|
251
304
|
]);
|
|
252
305
|
|
|
253
306
|
return {
|
|
254
307
|
...response,
|
|
308
|
+
issueTitle,
|
|
255
309
|
issueText: issueText.trim(),
|
|
310
|
+
planText: planText.trim(),
|
|
256
311
|
receipts,
|
|
257
312
|
transcriptLog
|
|
258
313
|
};
|
|
259
314
|
}
|
|
260
315
|
|
|
316
|
+
async function removeEmptyStaleWorktreeDirectory(paths) {
|
|
317
|
+
try {
|
|
318
|
+
const entries = await readdir(paths.worktree);
|
|
319
|
+
if (entries.length > 0) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
message: `Worktree path exists but is not a registered Git worktree: ${paths.worktree}`
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
await rmdir(paths.worktree);
|
|
326
|
+
return {
|
|
327
|
+
ok: true
|
|
328
|
+
};
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error?.code === "ENOENT") {
|
|
331
|
+
return {
|
|
332
|
+
ok: true
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
message: `Cannot prepare worktree path ${paths.worktree}: ${error?.message || error}`
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
261
342
|
async function createWorktree(paths, _options = {}, context = {}) {
|
|
262
343
|
const preconditions = context.preconditions || [];
|
|
263
344
|
if (await hasWorktree(paths)) {
|
|
@@ -268,7 +349,16 @@ async function createWorktree(paths, _options = {}, context = {}) {
|
|
|
268
349
|
});
|
|
269
350
|
}
|
|
270
351
|
|
|
271
|
-
await mkdir(paths.
|
|
352
|
+
await mkdir(path.dirname(paths.worktree), { recursive: true });
|
|
353
|
+
const staleWorktree = await removeEmptyStaleWorktreeDirectory(paths);
|
|
354
|
+
if (!staleWorktree.ok) {
|
|
355
|
+
return failSession(paths, {
|
|
356
|
+
code: "worktree_path_blocked",
|
|
357
|
+
message: staleWorktree.message,
|
|
358
|
+
repairCommand: `ls -la ${paths.worktree}`,
|
|
359
|
+
preconditions
|
|
360
|
+
});
|
|
361
|
+
}
|
|
272
362
|
const result = await runGit(paths.targetRoot, ["worktree", "add", "-b", paths.branch, paths.worktree, "HEAD"], {
|
|
273
363
|
timeout: 30000
|
|
274
364
|
});
|
|
@@ -287,6 +377,63 @@ async function createWorktree(paths, _options = {}, context = {}) {
|
|
|
287
377
|
});
|
|
288
378
|
}
|
|
289
379
|
|
|
380
|
+
async function recordDependenciesInstalled(paths, {
|
|
381
|
+
message = "Installed Node dependencies in the session worktree.",
|
|
382
|
+
preconditions = []
|
|
383
|
+
} = {}) {
|
|
384
|
+
await writeReceipt(paths, "dependencies_installed", message);
|
|
385
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
386
|
+
return buildSessionResponse(paths, {
|
|
387
|
+
preconditions
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function installDependencies(paths, _options = {}, context = {}) {
|
|
392
|
+
const preconditions = context.preconditions || [];
|
|
393
|
+
const result = await runCommand("npm", ["install"], {
|
|
394
|
+
cwd: paths.worktree,
|
|
395
|
+
timeout: 1000 * 60 * 10
|
|
396
|
+
});
|
|
397
|
+
if (!result.ok) {
|
|
398
|
+
return failSession(paths, {
|
|
399
|
+
code: "dependencies_install_failed",
|
|
400
|
+
message: result.output || "npm install failed in the session worktree.",
|
|
401
|
+
repairCommand: `cd ${paths.worktree} && npm install`,
|
|
402
|
+
preconditions
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return recordDependenciesInstalled(paths, {
|
|
406
|
+
message: result.output || "Installed Node dependencies in the session worktree.",
|
|
407
|
+
preconditions
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function adoptDependenciesInstalled({
|
|
412
|
+
targetRoot = process.cwd(),
|
|
413
|
+
sessionId,
|
|
414
|
+
message = ""
|
|
415
|
+
} = {}) {
|
|
416
|
+
return withExistingSession({ targetRoot, sessionId }, async (paths, context = {}) => {
|
|
417
|
+
const artifacts = await readSessionArtifacts(paths);
|
|
418
|
+
if (artifacts.nextStep !== "dependencies_installed") {
|
|
419
|
+
return buildSessionResponse(paths, {
|
|
420
|
+
ok: false,
|
|
421
|
+
errors: [
|
|
422
|
+
createError({
|
|
423
|
+
code: "session_step_mismatch",
|
|
424
|
+
message: `Cannot record dependencies for ${paths.sessionId}; current step is ${artifacts.nextStep || "complete"}.`
|
|
425
|
+
})
|
|
426
|
+
],
|
|
427
|
+
preconditions: context.preconditions || []
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return recordDependenciesInstalled(paths, {
|
|
431
|
+
message,
|
|
432
|
+
preconditions: context.preconditions || []
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
290
437
|
async function renderIssuePrompt(paths, options = {}) {
|
|
291
438
|
const userInput = normalizeText(options.prompt);
|
|
292
439
|
if (!userInput) {
|
|
@@ -299,7 +446,7 @@ async function renderIssuePrompt(paths, options = {}) {
|
|
|
299
446
|
const prompt = await renderPrompt(paths, "new_issue.md", {
|
|
300
447
|
user_input: userInput
|
|
301
448
|
});
|
|
302
|
-
await
|
|
449
|
+
await writePromptArtifact(paths, "issue_draft.md", prompt);
|
|
303
450
|
await writeReceipt(paths, "issue_prompt_rendered", "Rendered the issue drafting prompt.");
|
|
304
451
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
305
452
|
return buildSessionResponse(paths, {
|
|
@@ -318,7 +465,9 @@ async function draftIssue(paths, options = {}) {
|
|
|
318
465
|
repairCommand: `jskit session ${paths.sessionId} step --issue -`
|
|
319
466
|
});
|
|
320
467
|
}
|
|
468
|
+
const issueTitle = normalizeText(options.issueTitle) || extractIssueTitle(options.issue) || titleFromIssue(issueText);
|
|
321
469
|
await writeTextFile(path.join(paths.sessionRoot, "issue.md"), issueText);
|
|
470
|
+
await writeTextFile(path.join(paths.sessionRoot, "issue_title"), issueTitle);
|
|
322
471
|
await writeReceipt(paths, "issue_drafted", "Saved approved issue text.");
|
|
323
472
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
324
473
|
return buildSessionResponse(paths);
|
|
@@ -335,11 +484,12 @@ function titleFromIssue(issueText) {
|
|
|
335
484
|
async function createIssue(paths, _options = {}, context = {}) {
|
|
336
485
|
const preconditions = context.preconditions || [];
|
|
337
486
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
487
|
+
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
338
488
|
const result = await runCommand("gh", [
|
|
339
489
|
"issue",
|
|
340
490
|
"create",
|
|
341
491
|
"--title",
|
|
342
|
-
|
|
492
|
+
issueTitle,
|
|
343
493
|
"--body-file",
|
|
344
494
|
path.join(paths.sessionRoot, "issue.md")
|
|
345
495
|
], {
|
|
@@ -363,18 +513,65 @@ async function createIssue(paths, _options = {}, context = {}) {
|
|
|
363
513
|
});
|
|
364
514
|
}
|
|
365
515
|
|
|
366
|
-
async function
|
|
516
|
+
async function makePlan(paths, options = {}, context = {}) {
|
|
517
|
+
const preconditions = context.preconditions || [];
|
|
367
518
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
519
|
+
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
368
520
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
521
|
+
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
522
|
+
const planText = extractPlanText(options.plan);
|
|
523
|
+
|
|
524
|
+
if (!planText) {
|
|
525
|
+
const prompt = await renderPrompt(paths, "plan_issue.md", {
|
|
526
|
+
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
527
|
+
issue_number: issueNumber,
|
|
528
|
+
issue_text: issueText,
|
|
529
|
+
issue_title: issueTitle,
|
|
530
|
+
issue_title_file: path.join(paths.sessionRoot, "issue_title"),
|
|
531
|
+
issue_url: issueUrl,
|
|
532
|
+
plan_file: path.join(paths.sessionRoot, "plan.md"),
|
|
533
|
+
worktree: paths.worktree
|
|
534
|
+
});
|
|
535
|
+
await writePromptArtifact(paths, "plan_request.md", prompt);
|
|
536
|
+
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
537
|
+
return buildSessionResponse(paths, {
|
|
538
|
+
ok: true,
|
|
539
|
+
preconditions,
|
|
540
|
+
prompt,
|
|
541
|
+
status: SESSION_STATUS.WAITING_FOR_USER
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const planPath = path.join(paths.sessionRoot, "plan.md");
|
|
546
|
+
await writeTextFile(planPath, planText);
|
|
547
|
+
const commentResult = await runCommand("gh", ["issue", "comment", issueUrl, "--body-file", planPath], {
|
|
548
|
+
cwd: paths.targetRoot,
|
|
549
|
+
timeout: 1000 * 60
|
|
550
|
+
});
|
|
551
|
+
if (!commentResult.ok) {
|
|
552
|
+
return failSession(paths, {
|
|
553
|
+
code: "plan_comment_failed",
|
|
554
|
+
message: commentResult.output || "Failed to comment the implementation plan on the GitHub issue.",
|
|
555
|
+
repairCommand: `gh issue comment ${issueUrl} --body-file ${planPath}`,
|
|
556
|
+
preconditions
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
const executionPrompt = await renderPrompt(paths, "execute_plan.md", {
|
|
560
|
+
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
561
|
+
issue_number: issueNumber,
|
|
562
|
+
issue_title: issueTitle,
|
|
563
|
+
issue_url: issueUrl,
|
|
564
|
+
plan_file: planPath,
|
|
565
|
+
plan_text: planText,
|
|
566
|
+
worktree: paths.worktree
|
|
372
567
|
});
|
|
373
|
-
await
|
|
374
|
-
await writeReceipt(paths, "
|
|
568
|
+
await writePromptArtifact(paths, "plan_execution.md", executionPrompt);
|
|
569
|
+
await writeReceipt(paths, "plan_made", `Saved plan and commented on ${issueUrl}.`);
|
|
375
570
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
376
571
|
return buildSessionResponse(paths, {
|
|
377
|
-
|
|
572
|
+
codex: PLAN_EXECUTION_CODEX_HANDOFF,
|
|
573
|
+
preconditions,
|
|
574
|
+
prompt: executionPrompt,
|
|
378
575
|
status: SESSION_STATUS.WAITING_FOR_USER
|
|
379
576
|
});
|
|
380
577
|
}
|
|
@@ -396,7 +593,110 @@ async function worktreeStatus(worktree) {
|
|
|
396
593
|
};
|
|
397
594
|
}
|
|
398
595
|
|
|
399
|
-
async function
|
|
596
|
+
async function untrackedFiles(worktree) {
|
|
597
|
+
const result = await runGitInWorktree(worktree, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
598
|
+
timeout: 15000
|
|
599
|
+
});
|
|
600
|
+
if (!result.ok) {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
return result.stdout
|
|
604
|
+
.split("\0")
|
|
605
|
+
.filter((line) => line.length > 0);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function untrackedFileDiff(worktree, filePath) {
|
|
609
|
+
const result = await runGitInWorktree(worktree, [
|
|
610
|
+
"diff",
|
|
611
|
+
"--no-color",
|
|
612
|
+
"--no-ext-diff",
|
|
613
|
+
"--no-index",
|
|
614
|
+
"--",
|
|
615
|
+
"/dev/null",
|
|
616
|
+
filePath
|
|
617
|
+
], {
|
|
618
|
+
timeout: 15000
|
|
619
|
+
});
|
|
620
|
+
if (result.ok || result.exitCode === 1) {
|
|
621
|
+
return result.stdout;
|
|
622
|
+
}
|
|
623
|
+
return "";
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function untrackedFilesDiff(worktree) {
|
|
627
|
+
const diffs = [];
|
|
628
|
+
for (const filePath of await untrackedFiles(worktree)) {
|
|
629
|
+
const diff = await untrackedFileDiff(worktree, filePath);
|
|
630
|
+
if (diff) {
|
|
631
|
+
diffs.push(diff);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return diffs.join("\n");
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function inspectSessionDiff({
|
|
638
|
+
targetRoot = process.cwd(),
|
|
639
|
+
sessionId
|
|
640
|
+
} = {}) {
|
|
641
|
+
return withExistingSession({ targetRoot, sessionId }, async (paths) => {
|
|
642
|
+
const session = await buildSessionResponse(paths);
|
|
643
|
+
if (!await hasWorktree(paths)) {
|
|
644
|
+
return {
|
|
645
|
+
...session,
|
|
646
|
+
ok: false,
|
|
647
|
+
errors: [
|
|
648
|
+
createError({
|
|
649
|
+
code: "worktree_missing",
|
|
650
|
+
message: "Session worktree is not available for diff inspection."
|
|
651
|
+
})
|
|
652
|
+
],
|
|
653
|
+
gitStatus: "",
|
|
654
|
+
hasChanges: false,
|
|
655
|
+
stagedDiff: "",
|
|
656
|
+
unstagedDiff: "",
|
|
657
|
+
untrackedDiff: ""
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const [status, unstagedDiff, stagedDiff] = await Promise.all([
|
|
662
|
+
runGitInWorktree(paths.worktree, ["status", "--porcelain=v1"], { timeout: 15000 }),
|
|
663
|
+
runGitInWorktree(paths.worktree, ["diff", "--no-color", "--no-ext-diff"], { timeout: 30000 }),
|
|
664
|
+
runGitInWorktree(paths.worktree, ["diff", "--cached", "--no-color", "--no-ext-diff"], { timeout: 30000 })
|
|
665
|
+
]);
|
|
666
|
+
|
|
667
|
+
if (!status.ok || !unstagedDiff.ok || !stagedDiff.ok) {
|
|
668
|
+
return {
|
|
669
|
+
...session,
|
|
670
|
+
ok: false,
|
|
671
|
+
errors: [
|
|
672
|
+
createError({
|
|
673
|
+
code: "session_diff_failed",
|
|
674
|
+
message: [status, unstagedDiff, stagedDiff].find((result) => !result.ok)?.output ||
|
|
675
|
+
"Failed to inspect session worktree diff."
|
|
676
|
+
})
|
|
677
|
+
],
|
|
678
|
+
gitStatus: status.stdout || "",
|
|
679
|
+
hasChanges: false,
|
|
680
|
+
stagedDiff: stagedDiff.stdout || "",
|
|
681
|
+
unstagedDiff: unstagedDiff.stdout || "",
|
|
682
|
+
untrackedDiff: ""
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const untrackedDiff = await untrackedFilesDiff(paths.worktree);
|
|
687
|
+
return {
|
|
688
|
+
...session,
|
|
689
|
+
gitStatus: status.stdout,
|
|
690
|
+
hasChanges: Boolean(status.stdout.trim()),
|
|
691
|
+
stagedDiff: stagedDiff.stdout,
|
|
692
|
+
unstagedDiff: unstagedDiff.stdout,
|
|
693
|
+
untrackedDiff,
|
|
694
|
+
worktree: paths.worktree
|
|
695
|
+
};
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function acceptImplementationChanges(paths) {
|
|
400
700
|
const status = await worktreeStatus(paths.worktree);
|
|
401
701
|
if (!status.ok) {
|
|
402
702
|
return failSession(paths, {
|
|
@@ -408,11 +708,11 @@ async function detectChanges(paths) {
|
|
|
408
708
|
if (status.changedFiles.length < 1) {
|
|
409
709
|
return failSession(paths, {
|
|
410
710
|
code: "changes_missing",
|
|
411
|
-
message: "No worktree changes found.
|
|
711
|
+
message: "No worktree changes found. Ask Codex to implement the approved plan, inspect the worktree, then accept changes once ready.",
|
|
412
712
|
repairCommand: `jskit session ${paths.sessionId} step`
|
|
413
713
|
});
|
|
414
714
|
}
|
|
415
|
-
await writeReceipt(paths, "
|
|
715
|
+
await writeReceipt(paths, "implementation_changes_accepted", `Accepted ${status.changedFiles.length} changed file entries for commit.`);
|
|
416
716
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
417
717
|
return buildSessionResponse(paths);
|
|
418
718
|
}
|
|
@@ -482,83 +782,73 @@ async function changedFilesFromLastCommit(paths) {
|
|
|
482
782
|
return result.stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).join("\n");
|
|
483
783
|
}
|
|
484
784
|
|
|
485
|
-
function
|
|
486
|
-
return passNumber === 1
|
|
487
|
-
? "initial_review_prompt_rendered"
|
|
488
|
-
: passNumber === 2
|
|
489
|
-
? "followup_review_prompt_rendered"
|
|
490
|
-
: "final_review_prompt_rendered";
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
async function renderReviewPrompt(paths, passNumber) {
|
|
785
|
+
async function renderReviewPrompt(paths) {
|
|
494
786
|
const prompt = await renderPrompt(paths, "review_changes.md", {
|
|
495
|
-
changed_files: await changedFilesFromLastCommit(paths)
|
|
496
|
-
review_pass: String(passNumber),
|
|
497
|
-
review_pass_note: passNumber >= 3 ? "This is the final review pass before doctor and PR steps." : ""
|
|
787
|
+
changed_files: await changedFilesFromLastCommit(paths)
|
|
498
788
|
});
|
|
499
|
-
await
|
|
500
|
-
await writeReceipt(paths,
|
|
789
|
+
await writePromptArtifact(paths, "review.md", prompt);
|
|
790
|
+
await writeReceipt(paths, "review_prompt_rendered", "Started code review.");
|
|
501
791
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
502
792
|
return buildSessionResponse(paths, {
|
|
793
|
+
codex: REVIEW_EXECUTION_CODEX_HANDOFF,
|
|
503
794
|
prompt,
|
|
504
795
|
status: SESSION_STATUS.WAITING_FOR_USER
|
|
505
796
|
});
|
|
506
797
|
}
|
|
507
798
|
|
|
508
|
-
function
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
: "
|
|
799
|
+
async function acceptReviewChanges(paths) {
|
|
800
|
+
const status = await worktreeStatus(paths.worktree);
|
|
801
|
+
if (!status.ok) {
|
|
802
|
+
return failSession(paths, {
|
|
803
|
+
code: "git_status_failed",
|
|
804
|
+
message: status.output || "Failed to inspect review changes.",
|
|
805
|
+
repairCommand: `git -C ${paths.worktree} status --short`
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
const message = status.changedFiles.length > 0
|
|
809
|
+
? `Accepted ${status.changedFiles.length} review changed file entries for commit.`
|
|
810
|
+
: "Accepted review with no file changes.";
|
|
811
|
+
await writeReceipt(paths, "review_changes_accepted", message);
|
|
812
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
813
|
+
return buildSessionResponse(paths);
|
|
514
814
|
}
|
|
515
815
|
|
|
516
|
-
async function
|
|
816
|
+
async function commitReviewChanges(paths) {
|
|
517
817
|
const result = await commitWorktree(paths, {
|
|
518
818
|
allowNoChanges: true,
|
|
519
|
-
message: `Apply review
|
|
819
|
+
message: `Apply review changes for ${paths.sessionId}`
|
|
520
820
|
});
|
|
521
821
|
if (!result.ok) {
|
|
522
822
|
return failSession(paths, {
|
|
523
823
|
code: "review_commit_failed",
|
|
524
|
-
message: result.output ||
|
|
824
|
+
message: result.output || "Failed to commit review changes.",
|
|
525
825
|
repairCommand: `git -C ${paths.worktree} status --short`
|
|
526
826
|
});
|
|
527
827
|
}
|
|
528
828
|
const message = result.changedFiles?.length
|
|
529
|
-
?
|
|
530
|
-
:
|
|
531
|
-
await writeReceipt(paths,
|
|
829
|
+
? "Committed review changes."
|
|
830
|
+
: "No review changes detected.";
|
|
831
|
+
await writeReceipt(paths, "review_changes_committed", message);
|
|
532
832
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
533
833
|
return buildSessionResponse(paths);
|
|
534
834
|
}
|
|
535
835
|
|
|
536
|
-
function
|
|
537
|
-
return passNumber === 1
|
|
538
|
-
? "initial_user_check_completed"
|
|
539
|
-
: passNumber === 2
|
|
540
|
-
? "followup_user_check_completed"
|
|
541
|
-
: "final_user_check_completed";
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
async function userCheck(paths, passNumber, options = {}) {
|
|
836
|
+
async function userCheck(paths, options = {}) {
|
|
545
837
|
const result = normalizeText(options.userCheck || options["user-check"]).toLowerCase();
|
|
546
838
|
if (result === "passed" || result === "pass" || result === "ok" || result === "yes") {
|
|
547
|
-
await writeReceipt(paths,
|
|
839
|
+
await writeReceipt(paths, "user_check_completed", "User confirmed check passed.");
|
|
548
840
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
549
841
|
return buildSessionResponse(paths);
|
|
550
842
|
}
|
|
551
843
|
if (result === "failed" || result === "fail" || result === "no") {
|
|
552
844
|
return failSession(paths, {
|
|
553
845
|
code: "user_check_failed",
|
|
554
|
-
message:
|
|
846
|
+
message: "User check was reported as failed. Continue in Codex, then retry this step with --user-check passed.",
|
|
555
847
|
repairCommand: `jskit session ${paths.sessionId} step --user-check passed`
|
|
556
848
|
});
|
|
557
849
|
}
|
|
558
|
-
const prompt = await renderPrompt(paths, "user_check.md"
|
|
559
|
-
|
|
560
|
-
});
|
|
561
|
-
await writeTextFile(path.join(paths.sessionRoot, "prompt.md"), prompt);
|
|
850
|
+
const prompt = await renderPrompt(paths, "user_check.md");
|
|
851
|
+
await writePromptArtifact(paths, "user_check.md", prompt);
|
|
562
852
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
563
853
|
return buildSessionResponse(paths, {
|
|
564
854
|
prompt,
|
|
@@ -597,7 +887,7 @@ async function runDoctor(paths) {
|
|
|
597
887
|
const prompt = await renderPrompt(paths, "doctor_failure.md", {
|
|
598
888
|
doctor_output: result.output
|
|
599
889
|
});
|
|
600
|
-
await
|
|
890
|
+
await writePromptArtifact(paths, "doctor_failure.md", prompt);
|
|
601
891
|
return failSession(paths, {
|
|
602
892
|
code: "doctor_failed",
|
|
603
893
|
message: "Doctor/verification command failed. Paste the failure prompt into Codex, then rerun this step.",
|
|
@@ -610,30 +900,25 @@ async function runDoctor(paths) {
|
|
|
610
900
|
return buildSessionResponse(paths);
|
|
611
901
|
}
|
|
612
902
|
|
|
613
|
-
|
|
614
|
-
const
|
|
903
|
+
function issueNumberFromUrl(issueUrl) {
|
|
904
|
+
const match = /\/issues\/(\d+)(?:\b|$)/u.exec(String(issueUrl || ""));
|
|
905
|
+
return match ? match[1] : "";
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function createPr(paths) {
|
|
909
|
+
const pushResult = await runGitInWorktree(paths.worktree, ["push", "-u", "origin", "HEAD"], {
|
|
615
910
|
timeout: 1000 * 60 * 5
|
|
616
911
|
});
|
|
617
|
-
if (!
|
|
912
|
+
if (!pushResult.ok) {
|
|
618
913
|
return failSession(paths, {
|
|
619
914
|
code: "branch_push_failed",
|
|
620
|
-
message:
|
|
915
|
+
message: pushResult.output || "Failed to push session branch.",
|
|
621
916
|
repairCommand: `git -C ${paths.worktree} push -u origin HEAD`
|
|
622
917
|
});
|
|
623
918
|
}
|
|
624
|
-
await writeReceipt(paths, "branch_pushed", `Pushed branch ${paths.branch}.`);
|
|
625
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
626
|
-
return buildSessionResponse(paths);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function issueNumberFromUrl(issueUrl) {
|
|
630
|
-
const match = /\/issues\/(\d+)(?:\b|$)/u.exec(String(issueUrl || ""));
|
|
631
|
-
return match ? match[1] : "";
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
async function createPr(paths) {
|
|
635
919
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
636
920
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
921
|
+
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
637
922
|
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
638
923
|
const body = [
|
|
639
924
|
issueNumber ? `Closes #${issueNumber}` : "",
|
|
@@ -646,7 +931,7 @@ async function createPr(paths) {
|
|
|
646
931
|
"pr",
|
|
647
932
|
"create",
|
|
648
933
|
"--title",
|
|
649
|
-
|
|
934
|
+
issueTitle,
|
|
650
935
|
"--body-file",
|
|
651
936
|
bodyPath
|
|
652
937
|
], {
|
|
@@ -657,7 +942,7 @@ async function createPr(paths) {
|
|
|
657
942
|
const prompt = await renderPrompt(paths, "pr_failure.md", {
|
|
658
943
|
doctor_output: result.output
|
|
659
944
|
});
|
|
660
|
-
await
|
|
945
|
+
await writePromptArtifact(paths, "pr_create_failure.md", prompt);
|
|
661
946
|
return failSession(paths, {
|
|
662
947
|
code: "pr_create_failed",
|
|
663
948
|
message: result.output || "Failed to create PR.",
|
|
@@ -667,42 +952,41 @@ async function createPr(paths) {
|
|
|
667
952
|
}
|
|
668
953
|
const prUrl = result.stdout.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || result.stdout;
|
|
669
954
|
await writeTextFile(path.join(paths.sessionRoot, "pr_url"), prUrl);
|
|
670
|
-
await writeReceipt(paths, "pr_created", `
|
|
955
|
+
await writeReceipt(paths, "pr_created", `Pushed branch ${paths.branch} and created PR ${prUrl}.`);
|
|
671
956
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
672
957
|
return buildSessionResponse(paths);
|
|
673
958
|
}
|
|
674
959
|
|
|
675
960
|
async function mergePr(paths) {
|
|
676
961
|
const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (!mergeResult.ok) {
|
|
682
|
-
const prompt = await renderPrompt(paths, "pr_failure.md", {
|
|
683
|
-
doctor_output: mergeResult.output
|
|
684
|
-
});
|
|
685
|
-
await writeTextFile(path.join(paths.sessionRoot, "prompt.md"), prompt);
|
|
686
|
-
return failSession(paths, {
|
|
687
|
-
code: "pr_merge_failed",
|
|
688
|
-
message: mergeResult.output || "Failed to merge PR.",
|
|
689
|
-
repairCommand: `gh pr merge ${prUrl} --merge --delete-branch`,
|
|
690
|
-
prompt
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
694
|
-
if (issueUrl) {
|
|
695
|
-
await runCommand("gh", ["issue", "close", issueUrl, "--comment", `Merged PR ${prUrl}.`], {
|
|
962
|
+
const mergeMarkerPath = path.join(paths.sessionRoot, "pr_merge_completed");
|
|
963
|
+
const mergeAlreadyCompleted = await readTrimmedFile(mergeMarkerPath);
|
|
964
|
+
if (!mergeAlreadyCompleted) {
|
|
965
|
+
const mergeResult = await runCommand("gh", ["pr", "merge", prUrl, "--merge", "--delete-branch"], {
|
|
696
966
|
cwd: paths.worktree,
|
|
697
|
-
timeout: 1000 * 60
|
|
967
|
+
timeout: 1000 * 60 * 5
|
|
698
968
|
});
|
|
969
|
+
if (!mergeResult.ok) {
|
|
970
|
+
const prompt = await renderPrompt(paths, "pr_failure.md", {
|
|
971
|
+
doctor_output: mergeResult.output
|
|
972
|
+
});
|
|
973
|
+
await writePromptArtifact(paths, "pr_merge_failure.md", prompt);
|
|
974
|
+
return failSession(paths, {
|
|
975
|
+
code: "pr_merge_failed",
|
|
976
|
+
message: mergeResult.output || "Failed to merge PR.",
|
|
977
|
+
repairCommand: `gh pr merge ${prUrl} --merge --delete-branch`,
|
|
978
|
+
prompt
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
982
|
+
if (issueUrl) {
|
|
983
|
+
await runCommand("gh", ["issue", "close", issueUrl, "--comment", `Merged PR ${prUrl}.`], {
|
|
984
|
+
cwd: paths.worktree,
|
|
985
|
+
timeout: 1000 * 60
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
await writeTextFile(mergeMarkerPath, `${prUrl}\n`);
|
|
699
989
|
}
|
|
700
|
-
await writeReceipt(paths, "pr_merged", `Merged PR ${prUrl}.`);
|
|
701
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
702
|
-
return buildSessionResponse(paths);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
async function removeWorktree(paths) {
|
|
706
990
|
if (await hasWorktree(paths)) {
|
|
707
991
|
const result = await runGit(paths.targetRoot, ["worktree", "remove", paths.worktree], {
|
|
708
992
|
timeout: 1000 * 60
|
|
@@ -715,7 +999,7 @@ async function removeWorktree(paths) {
|
|
|
715
999
|
});
|
|
716
1000
|
}
|
|
717
1001
|
}
|
|
718
|
-
await writeReceipt(paths, "
|
|
1002
|
+
await writeReceipt(paths, "pr_merged", `Merged PR ${prUrl} and removed worktree ${paths.worktree}.`);
|
|
719
1003
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
720
1004
|
return buildSessionResponse(paths);
|
|
721
1005
|
}
|
|
@@ -748,26 +1032,20 @@ async function finishSession(paths) {
|
|
|
748
1032
|
|
|
749
1033
|
const STEP_RUNNERS = Object.freeze({
|
|
750
1034
|
worktree_created: createWorktree,
|
|
1035
|
+
dependencies_installed: installDependencies,
|
|
751
1036
|
issue_prompt_rendered: renderIssuePrompt,
|
|
752
1037
|
issue_drafted: draftIssue,
|
|
753
1038
|
issue_created: createIssue,
|
|
754
|
-
|
|
755
|
-
|
|
1039
|
+
plan_made: makePlan,
|
|
1040
|
+
implementation_changes_accepted: acceptImplementationChanges,
|
|
756
1041
|
implementation_changes_committed: commitImplementation,
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
followup_review_changes_detected: (paths) => detectAndCommitReviewChanges(paths, 2),
|
|
762
|
-
followup_user_check_completed: (paths, options) => userCheck(paths, 2, options),
|
|
763
|
-
final_review_prompt_rendered: (paths) => renderReviewPrompt(paths, 3),
|
|
764
|
-
final_review_changes_detected: (paths) => detectAndCommitReviewChanges(paths, 3),
|
|
765
|
-
final_user_check_completed: (paths, options) => userCheck(paths, 3, options),
|
|
1042
|
+
review_prompt_rendered: renderReviewPrompt,
|
|
1043
|
+
review_changes_accepted: acceptReviewChanges,
|
|
1044
|
+
review_changes_committed: commitReviewChanges,
|
|
1045
|
+
user_check_completed: userCheck,
|
|
766
1046
|
doctor_run: runDoctor,
|
|
767
|
-
branch_pushed: pushBranch,
|
|
768
1047
|
pr_created: createPr,
|
|
769
1048
|
pr_merged: mergePr,
|
|
770
|
-
worktree_removed: removeWorktree,
|
|
771
1049
|
session_finished: finishSession
|
|
772
1050
|
});
|
|
773
1051
|
|
|
@@ -776,8 +1054,8 @@ const PRECONDITION_RUNNERS = Object.freeze({
|
|
|
776
1054
|
git_repository: (paths) => assertGitRepository(paths.targetRoot),
|
|
777
1055
|
github_auth: (paths) => assertGhAuth(paths.targetRoot),
|
|
778
1056
|
github_origin: (paths) => assertGithubOrigin(paths.targetRoot),
|
|
779
|
-
issue_artifacts: assertIssueArtifacts,
|
|
780
1057
|
issue_text_exists: assertIssueTextExists,
|
|
1058
|
+
issue_url_exists: assertIssueUrlExists,
|
|
781
1059
|
pr_url_exists: assertPrUrlExists,
|
|
782
1060
|
session_exists: assertSessionExists,
|
|
783
1061
|
worktree_exists: assertWorktreeExists
|
|
@@ -850,10 +1128,18 @@ async function abandonSession({
|
|
|
850
1128
|
}
|
|
851
1129
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
852
1130
|
if (issueUrl) {
|
|
853
|
-
await runCommand("gh", ["issue", "close", issueUrl, "--comment", `Abandoned JSKIT Studio session ${paths.sessionId}.`], {
|
|
1131
|
+
const closeIssueResult = await runCommand("gh", ["issue", "close", issueUrl, "--comment", `Abandoned JSKIT Studio session ${paths.sessionId}.`], {
|
|
854
1132
|
cwd: paths.targetRoot,
|
|
855
1133
|
timeout: 1000 * 60
|
|
856
1134
|
});
|
|
1135
|
+
if (!closeIssueResult.ok) {
|
|
1136
|
+
return failSession(paths, {
|
|
1137
|
+
code: "issue_close_failed",
|
|
1138
|
+
message: closeIssueResult.output || "Failed to close GitHub issue for abandoned session.",
|
|
1139
|
+
repairCommand: `gh issue close ${issueUrl}`,
|
|
1140
|
+
status: SESSION_STATUS.FAILED
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
857
1143
|
}
|
|
858
1144
|
if (await hasWorktree(paths)) {
|
|
859
1145
|
await runGit(paths.targetRoot, ["worktree", "remove", "--force", paths.worktree], {
|
|
@@ -911,17 +1197,22 @@ export {
|
|
|
911
1197
|
STEP_IDS,
|
|
912
1198
|
STEP_PRECONDITION_NAMES,
|
|
913
1199
|
abandonSession,
|
|
1200
|
+
adoptDependenciesInstalled,
|
|
914
1201
|
adoptCodexThreadId,
|
|
915
1202
|
buildSessionResponse,
|
|
916
1203
|
buildSessionErrorResponse,
|
|
917
1204
|
createSession,
|
|
918
1205
|
createSessionId,
|
|
1206
|
+
extractIssueTitle,
|
|
919
1207
|
extractIssueText,
|
|
1208
|
+
extractPlanText,
|
|
920
1209
|
inspectSession,
|
|
1210
|
+
inspectSessionDiff,
|
|
921
1211
|
inspectSessionDetails,
|
|
922
1212
|
isValidSessionId,
|
|
923
1213
|
listSessions,
|
|
924
1214
|
renderTemplate,
|
|
1215
|
+
recordDependenciesInstalled,
|
|
925
1216
|
resolveSessionPaths,
|
|
926
1217
|
runSessionStep
|
|
927
1218
|
};
|