@jskit-ai/jskit-cli 0.2.79 → 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.
@@ -0,0 +1,1218 @@
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ readdir,
5
+ rmdir
6
+ } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import {
9
+ PLAN_EXECUTION_CODEX_HANDOFF,
10
+ REVIEW_EXECUTION_CODEX_HANDOFF,
11
+ SESSION_STATUS,
12
+ STEP_DEFINITIONS,
13
+ STEP_IDS,
14
+ STEP_PRECONDITION_NAMES
15
+ } from "./sessionRuntime/constants.js";
16
+ import {
17
+ normalizeText,
18
+ readTextIfExists,
19
+ readTrimmedFile,
20
+ runCommand,
21
+ runGit,
22
+ runGitInWorktree,
23
+ timestampForReceipt,
24
+ writeTextFile
25
+ } from "./sessionRuntime/io.js";
26
+ import {
27
+ archiveSession,
28
+ createAvailableSessionId,
29
+ createSessionId,
30
+ isValidSessionId,
31
+ resolveExistingSessionRoot,
32
+ resolveSessionPaths,
33
+ pathsForExistingSession
34
+ } from "./sessionRuntime/paths.js";
35
+ import {
36
+ buildSessionErrorResponse,
37
+ buildSessionResponse,
38
+ buildStepDefinitions,
39
+ createError,
40
+ failSession,
41
+ markCurrentStep,
42
+ markStatus,
43
+ readReceiptSteps,
44
+ readSessionArtifacts,
45
+ writeReceipt
46
+ } from "./sessionRuntime/responses.js";
47
+ import {
48
+ applyPreconditions,
49
+ assertGhAuth,
50
+ assertGitCurrentBranch,
51
+ assertGitRepository,
52
+ assertGithubOrigin,
53
+ assertIssueTextExists,
54
+ assertIssueUrlExists,
55
+ assertPrUrlExists,
56
+ assertSessionExists,
57
+ assertTargetRootWritable,
58
+ assertWorktreeExists,
59
+ ensureStudioGitExclude,
60
+ hasWorktree
61
+ } from "./sessionRuntime/preconditions.js";
62
+ import {
63
+ renderPrompt,
64
+ renderTemplate
65
+ } from "./sessionRuntime/promptRenderer.js";
66
+
67
+ function invalidSessionIdError(sessionId = "") {
68
+ return createError({
69
+ code: "invalid_session_id",
70
+ message: `Invalid session id "${sessionId}". Expected YYYY-MM-DD_HH-MM-SS.`
71
+ });
72
+ }
73
+
74
+ function invalidSessionIdResponse({
75
+ targetRoot,
76
+ sessionId
77
+ }) {
78
+ return buildSessionErrorResponse({
79
+ targetRoot,
80
+ sessionId,
81
+ errors: [invalidSessionIdError(sessionId)]
82
+ });
83
+ }
84
+
85
+ async function existingSessionContext({
86
+ targetRoot = process.cwd(),
87
+ sessionId
88
+ } = {}) {
89
+ if (!isValidSessionId(sessionId)) {
90
+ return {
91
+ ok: false,
92
+ response: invalidSessionIdResponse({ targetRoot, sessionId })
93
+ };
94
+ }
95
+
96
+ const paths = await pathsForExistingSession(resolveSessionPaths({ targetRoot, sessionId }));
97
+ const preconditions = await applyPreconditions(paths, [
98
+ () => assertSessionExists(paths)
99
+ ]);
100
+ if (!preconditions.ok) {
101
+ return {
102
+ ok: false,
103
+ response: await failSession(paths, {
104
+ ...preconditions.error,
105
+ preconditions: preconditions.preconditions
106
+ })
107
+ };
108
+ }
109
+
110
+ return {
111
+ ok: true,
112
+ paths,
113
+ preconditions: preconditions.preconditions
114
+ };
115
+ }
116
+
117
+ async function withExistingSession(input, handler) {
118
+ const context = await existingSessionContext(input);
119
+ if (!context.ok) {
120
+ return context.response;
121
+ }
122
+ return handler(context.paths, {
123
+ preconditions: context.preconditions
124
+ });
125
+ }
126
+
127
+ function extractMarkedText(value = "", marker = "") {
128
+ const text = normalizeText(value);
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);
152
+ }
153
+
154
+ async function createSession({
155
+ targetRoot = process.cwd(),
156
+ sessionId = "",
157
+ now = new Date()
158
+ } = {}) {
159
+ if (sessionId && !isValidSessionId(sessionId)) {
160
+ return invalidSessionIdResponse({ targetRoot, sessionId });
161
+ }
162
+ const initialPaths = resolveSessionPaths({
163
+ targetRoot,
164
+ sessionId: sessionId || await createAvailableSessionId(targetRoot, now)
165
+ });
166
+ const existingSession = await resolveExistingSessionRoot(initialPaths);
167
+ if (existingSession.root) {
168
+ return failSession(initialPaths, {
169
+ code: "session_exists",
170
+ message: `Session already exists: ${initialPaths.sessionId}`,
171
+ status: SESSION_STATUS.BLOCKED
172
+ });
173
+ }
174
+
175
+ const preconditions = await applyPreconditions(initialPaths, [
176
+ () => assertTargetRootWritable(initialPaths.targetRoot),
177
+ () => assertGitRepository(initialPaths.targetRoot)
178
+ ]);
179
+ if (!preconditions.ok) {
180
+ return failSession(initialPaths, {
181
+ ...preconditions.error,
182
+ preconditions: preconditions.preconditions
183
+ });
184
+ }
185
+
186
+ await ensureStudioGitExclude(initialPaths.targetRoot);
187
+ await mkdir(initialPaths.sessionRoot, { recursive: true });
188
+ await writeTextFile(path.join(initialPaths.sessionRoot, "transcript.log"), "");
189
+ await markStatus(initialPaths, SESSION_STATUS.PENDING);
190
+ await writeReceipt(initialPaths, "session_created", `Created JSKIT Studio issue session ${initialPaths.sessionId}.`);
191
+
192
+ return buildSessionResponse(initialPaths, {
193
+ ok: true,
194
+ preconditions: preconditions.preconditions
195
+ });
196
+ }
197
+
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" } = {}) {
218
+ const paths = resolveSessionPaths({ targetRoot });
219
+ const sessions = [];
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
+ }));
230
+
231
+ for (const rootInfo of roots) {
232
+ let entries = [];
233
+ try {
234
+ entries = await readdir(rootInfo.root, { withFileTypes: true });
235
+ } catch {
236
+ entries = [];
237
+ }
238
+
239
+ for (const entry of entries) {
240
+ if (!entry.isDirectory() || !isValidSessionId(entry.name)) {
241
+ continue;
242
+ }
243
+ const sessionPaths = resolveSessionPaths({
244
+ targetRoot,
245
+ sessionId: entry.name
246
+ });
247
+ const response = await buildSessionResponse({
248
+ ...sessionPaths,
249
+ archive: rootInfo.archive,
250
+ sessionRoot: path.join(rootInfo.root, entry.name)
251
+ });
252
+ sessions.push(response);
253
+ }
254
+ }
255
+ sessions.sort((left, right) => right.sessionId.localeCompare(left.sessionId));
256
+ return {
257
+ archive: selectedArchives.length === 1 ? selectedArchives[0] : "mixed",
258
+ archives: selectedArchives,
259
+ ok: true,
260
+ stepDefinitions: buildStepDefinitions(),
261
+ sessions
262
+ };
263
+ }
264
+
265
+ async function inspectSession({
266
+ targetRoot = process.cwd(),
267
+ sessionId
268
+ } = {}) {
269
+ return withExistingSession({ targetRoot, sessionId }, (paths, context) => {
270
+ return buildSessionResponse(paths, {
271
+ preconditions: context.preconditions
272
+ });
273
+ });
274
+ }
275
+
276
+ function emptySessionDetails(response) {
277
+ return {
278
+ ...response,
279
+ issueTitle: "",
280
+ issueText: "",
281
+ planText: "",
282
+ receipts: [],
283
+ transcriptLog: ""
284
+ };
285
+ }
286
+
287
+ async function inspectSessionDetails({
288
+ targetRoot = process.cwd(),
289
+ sessionId
290
+ } = {}) {
291
+ const context = await existingSessionContext({ targetRoot, sessionId });
292
+ if (!context.ok) {
293
+ return emptySessionDetails(context.response);
294
+ }
295
+ const { paths, preconditions } = context;
296
+ const response = await buildSessionResponse(paths, { preconditions });
297
+
298
+ const [issueText, issueTitle, planText, receipts, transcriptLog] = await Promise.all([
299
+ readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
300
+ readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
301
+ readTextIfExists(path.join(paths.sessionRoot, "plan.md")),
302
+ readReceiptSteps(paths),
303
+ readTextIfExists(path.join(paths.sessionRoot, "transcript.log"))
304
+ ]);
305
+
306
+ return {
307
+ ...response,
308
+ issueTitle,
309
+ issueText: issueText.trim(),
310
+ planText: planText.trim(),
311
+ receipts,
312
+ transcriptLog
313
+ };
314
+ }
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
+
342
+ async function createWorktree(paths, _options = {}, context = {}) {
343
+ const preconditions = context.preconditions || [];
344
+ if (await hasWorktree(paths)) {
345
+ await writeReceipt(paths, "worktree_created", `Reused existing worktree ${paths.worktree}.`);
346
+ await markStatus(paths, SESSION_STATUS.RUNNING);
347
+ return buildSessionResponse(paths, {
348
+ preconditions
349
+ });
350
+ }
351
+
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
+ }
362
+ const result = await runGit(paths.targetRoot, ["worktree", "add", "-b", paths.branch, paths.worktree, "HEAD"], {
363
+ timeout: 30000
364
+ });
365
+ if (!result.ok) {
366
+ return failSession(paths, {
367
+ code: "worktree_create_failed",
368
+ message: result.output || `Failed to create worktree ${paths.worktree}.`,
369
+ repairCommand: `git worktree add -b ${paths.branch} ${paths.worktree} HEAD`,
370
+ preconditions
371
+ });
372
+ }
373
+ await writeReceipt(paths, "worktree_created", `Created worktree ${paths.worktree} on branch ${paths.branch}.`);
374
+ await markStatus(paths, SESSION_STATUS.RUNNING);
375
+ return buildSessionResponse(paths, {
376
+ preconditions
377
+ });
378
+ }
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
+
437
+ async function renderIssuePrompt(paths, options = {}) {
438
+ const userInput = normalizeText(options.prompt);
439
+ if (!userInput) {
440
+ return failSession(paths, {
441
+ code: "prompt_required",
442
+ message: "The issue prompt step requires --prompt.",
443
+ repairCommand: `jskit session ${paths.sessionId} step --prompt "<what should change>"`
444
+ });
445
+ }
446
+ const prompt = await renderPrompt(paths, "new_issue.md", {
447
+ user_input: userInput
448
+ });
449
+ await writePromptArtifact(paths, "issue_draft.md", prompt);
450
+ await writeReceipt(paths, "issue_prompt_rendered", "Rendered the issue drafting prompt.");
451
+ await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
452
+ return buildSessionResponse(paths, {
453
+ ok: true,
454
+ prompt,
455
+ status: SESSION_STATUS.WAITING_FOR_USER
456
+ });
457
+ }
458
+
459
+ async function draftIssue(paths, options = {}) {
460
+ const issueText = extractIssueText(options.issue);
461
+ if (!issueText) {
462
+ return failSession(paths, {
463
+ code: "issue_required",
464
+ message: "The issue drafting step requires --issue, --issue-file, or --issue -.",
465
+ repairCommand: `jskit session ${paths.sessionId} step --issue -`
466
+ });
467
+ }
468
+ const issueTitle = normalizeText(options.issueTitle) || extractIssueTitle(options.issue) || titleFromIssue(issueText);
469
+ await writeTextFile(path.join(paths.sessionRoot, "issue.md"), issueText);
470
+ await writeTextFile(path.join(paths.sessionRoot, "issue_title"), issueTitle);
471
+ await writeReceipt(paths, "issue_drafted", "Saved approved issue text.");
472
+ await markStatus(paths, SESSION_STATUS.RUNNING);
473
+ return buildSessionResponse(paths);
474
+ }
475
+
476
+ function titleFromIssue(issueText) {
477
+ const firstMeaningfulLine = String(issueText || "")
478
+ .split(/\r?\n/u)
479
+ .map((line) => line.replace(/^#+\s*/u, "").trim())
480
+ .find(Boolean);
481
+ return (firstMeaningfulLine || "JSKIT Studio issue").slice(0, 120);
482
+ }
483
+
484
+ async function createIssue(paths, _options = {}, context = {}) {
485
+ const preconditions = context.preconditions || [];
486
+ const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
487
+ const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
488
+ const result = await runCommand("gh", [
489
+ "issue",
490
+ "create",
491
+ "--title",
492
+ issueTitle,
493
+ "--body-file",
494
+ path.join(paths.sessionRoot, "issue.md")
495
+ ], {
496
+ cwd: paths.targetRoot,
497
+ timeout: 30000
498
+ });
499
+ if (!result.ok || !result.stdout) {
500
+ return failSession(paths, {
501
+ code: "issue_create_failed",
502
+ message: result.output || "GitHub issue creation failed.",
503
+ repairCommand: "gh issue create",
504
+ preconditions
505
+ });
506
+ }
507
+ const issueUrl = result.stdout.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || result.stdout;
508
+ await writeTextFile(path.join(paths.sessionRoot, "issue_url"), issueUrl);
509
+ await writeReceipt(paths, "issue_created", `Created GitHub issue ${issueUrl}.`);
510
+ await markStatus(paths, SESSION_STATUS.RUNNING);
511
+ return buildSessionResponse(paths, {
512
+ preconditions
513
+ });
514
+ }
515
+
516
+ async function makePlan(paths, options = {}, context = {}) {
517
+ const preconditions = context.preconditions || [];
518
+ const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
519
+ const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
520
+ const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
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
567
+ });
568
+ await writePromptArtifact(paths, "plan_execution.md", executionPrompt);
569
+ await writeReceipt(paths, "plan_made", `Saved plan and commented on ${issueUrl}.`);
570
+ await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
571
+ return buildSessionResponse(paths, {
572
+ codex: PLAN_EXECUTION_CODEX_HANDOFF,
573
+ preconditions,
574
+ prompt: executionPrompt,
575
+ status: SESSION_STATUS.WAITING_FOR_USER
576
+ });
577
+ }
578
+
579
+ async function worktreeStatus(worktree) {
580
+ const result = await runGitInWorktree(worktree, ["status", "--porcelain=v1"]);
581
+ if (!result.ok) {
582
+ return {
583
+ ok: false,
584
+ changedFiles: [],
585
+ output: result.output
586
+ };
587
+ }
588
+ const changedFiles = result.stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
589
+ return {
590
+ ok: true,
591
+ changedFiles,
592
+ output: result.stdout
593
+ };
594
+ }
595
+
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) {
700
+ const status = await worktreeStatus(paths.worktree);
701
+ if (!status.ok) {
702
+ return failSession(paths, {
703
+ code: "git_status_failed",
704
+ message: status.output || "Failed to inspect worktree changes.",
705
+ repairCommand: `git -C ${paths.worktree} status --short`
706
+ });
707
+ }
708
+ if (status.changedFiles.length < 1) {
709
+ return failSession(paths, {
710
+ code: "changes_missing",
711
+ message: "No worktree changes found. Ask Codex to implement the approved plan, inspect the worktree, then accept changes once ready.",
712
+ repairCommand: `jskit session ${paths.sessionId} step`
713
+ });
714
+ }
715
+ await writeReceipt(paths, "implementation_changes_accepted", `Accepted ${status.changedFiles.length} changed file entries for commit.`);
716
+ await markStatus(paths, SESSION_STATUS.RUNNING);
717
+ return buildSessionResponse(paths);
718
+ }
719
+
720
+ async function commitWorktree(paths, {
721
+ message,
722
+ allowNoChanges = false
723
+ } = {}) {
724
+ const status = await worktreeStatus(paths.worktree);
725
+ if (!status.ok) {
726
+ return {
727
+ ok: false,
728
+ output: status.output
729
+ };
730
+ }
731
+ if (status.changedFiles.length < 1) {
732
+ return {
733
+ changedFiles: [],
734
+ ok: allowNoChanges,
735
+ output: allowNoChanges ? "No changes to commit." : "No changes found."
736
+ };
737
+ }
738
+ const addResult = await runGitInWorktree(paths.worktree, ["add", "."]);
739
+ if (!addResult.ok) {
740
+ return {
741
+ ok: false,
742
+ output: addResult.output
743
+ };
744
+ }
745
+ const commitResult = await runGitInWorktree(paths.worktree, ["commit", "-m", message], {
746
+ timeout: 30000
747
+ });
748
+ if (!commitResult.ok) {
749
+ return {
750
+ ok: false,
751
+ output: commitResult.output
752
+ };
753
+ }
754
+ return {
755
+ changedFiles: status.changedFiles,
756
+ ok: true,
757
+ output: commitResult.output
758
+ };
759
+ }
760
+
761
+ async function commitImplementation(paths) {
762
+ const result = await commitWorktree(paths, {
763
+ message: `Implement JSKIT session ${paths.sessionId}`
764
+ });
765
+ if (!result.ok) {
766
+ return failSession(paths, {
767
+ code: "commit_failed",
768
+ message: result.output || "Failed to commit implementation changes.",
769
+ repairCommand: `git -C ${paths.worktree} status --short`
770
+ });
771
+ }
772
+ await writeReceipt(paths, "implementation_changes_committed", `Committed implementation changes for ${paths.sessionId}.`);
773
+ await markStatus(paths, SESSION_STATUS.RUNNING);
774
+ return buildSessionResponse(paths);
775
+ }
776
+
777
+ async function changedFilesFromLastCommit(paths) {
778
+ const result = await runGitInWorktree(paths.worktree, ["show", "--name-only", "--format=", "HEAD"]);
779
+ if (!result.ok) {
780
+ return "";
781
+ }
782
+ return result.stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).join("\n");
783
+ }
784
+
785
+ async function renderReviewPrompt(paths) {
786
+ const prompt = await renderPrompt(paths, "review_changes.md", {
787
+ changed_files: await changedFilesFromLastCommit(paths)
788
+ });
789
+ await writePromptArtifact(paths, "review.md", prompt);
790
+ await writeReceipt(paths, "review_prompt_rendered", "Started code review.");
791
+ await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
792
+ return buildSessionResponse(paths, {
793
+ codex: REVIEW_EXECUTION_CODEX_HANDOFF,
794
+ prompt,
795
+ status: SESSION_STATUS.WAITING_FOR_USER
796
+ });
797
+ }
798
+
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);
814
+ }
815
+
816
+ async function commitReviewChanges(paths) {
817
+ const result = await commitWorktree(paths, {
818
+ allowNoChanges: true,
819
+ message: `Apply review changes for ${paths.sessionId}`
820
+ });
821
+ if (!result.ok) {
822
+ return failSession(paths, {
823
+ code: "review_commit_failed",
824
+ message: result.output || "Failed to commit review changes.",
825
+ repairCommand: `git -C ${paths.worktree} status --short`
826
+ });
827
+ }
828
+ const message = result.changedFiles?.length
829
+ ? "Committed review changes."
830
+ : "No review changes detected.";
831
+ await writeReceipt(paths, "review_changes_committed", message);
832
+ await markStatus(paths, SESSION_STATUS.RUNNING);
833
+ return buildSessionResponse(paths);
834
+ }
835
+
836
+ async function userCheck(paths, options = {}) {
837
+ const result = normalizeText(options.userCheck || options["user-check"]).toLowerCase();
838
+ if (result === "passed" || result === "pass" || result === "ok" || result === "yes") {
839
+ await writeReceipt(paths, "user_check_completed", "User confirmed check passed.");
840
+ await markStatus(paths, SESSION_STATUS.RUNNING);
841
+ return buildSessionResponse(paths);
842
+ }
843
+ if (result === "failed" || result === "fail" || result === "no") {
844
+ return failSession(paths, {
845
+ code: "user_check_failed",
846
+ message: "User check was reported as failed. Continue in Codex, then retry this step with --user-check passed.",
847
+ repairCommand: `jskit session ${paths.sessionId} step --user-check passed`
848
+ });
849
+ }
850
+ const prompt = await renderPrompt(paths, "user_check.md");
851
+ await writePromptArtifact(paths, "user_check.md", prompt);
852
+ await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
853
+ return buildSessionResponse(paths, {
854
+ prompt,
855
+ status: SESSION_STATUS.WAITING_FOR_USER
856
+ });
857
+ }
858
+
859
+ async function readPackageJson(root) {
860
+ try {
861
+ return JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
862
+ } catch {
863
+ return {};
864
+ }
865
+ }
866
+
867
+ async function doctorCommandForWorktree(worktree) {
868
+ const packageJson = await readPackageJson(worktree);
869
+ const scripts = packageJson && typeof packageJson.scripts === "object" ? packageJson.scripts : {};
870
+ if (scripts["verify:local"]) {
871
+ return ["npm", ["run", "verify:local"]];
872
+ }
873
+ if (scripts.verify) {
874
+ return ["npm", ["run", "verify"]];
875
+ }
876
+ return ["npx", ["jskit", "app", "verify"]];
877
+ }
878
+
879
+ async function runDoctor(paths) {
880
+ const [command, args] = await doctorCommandForWorktree(paths.worktree);
881
+ const result = await runCommand(command, args, {
882
+ cwd: paths.worktree,
883
+ timeout: 1000 * 60 * 15
884
+ });
885
+ await writeTextFile(path.join(paths.sessionRoot, "doctor.log"), result.output);
886
+ if (!result.ok) {
887
+ const prompt = await renderPrompt(paths, "doctor_failure.md", {
888
+ doctor_output: result.output
889
+ });
890
+ await writePromptArtifact(paths, "doctor_failure.md", prompt);
891
+ return failSession(paths, {
892
+ code: "doctor_failed",
893
+ message: "Doctor/verification command failed. Paste the failure prompt into Codex, then rerun this step.",
894
+ repairCommand: `${command} ${args.join(" ")}`,
895
+ prompt
896
+ });
897
+ }
898
+ await writeReceipt(paths, "doctor_run", `Doctor command passed: ${command} ${args.join(" ")}.`);
899
+ await markStatus(paths, SESSION_STATUS.RUNNING);
900
+ return buildSessionResponse(paths);
901
+ }
902
+
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"], {
910
+ timeout: 1000 * 60 * 5
911
+ });
912
+ if (!pushResult.ok) {
913
+ return failSession(paths, {
914
+ code: "branch_push_failed",
915
+ message: pushResult.output || "Failed to push session branch.",
916
+ repairCommand: `git -C ${paths.worktree} push -u origin HEAD`
917
+ });
918
+ }
919
+ const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
920
+ const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
921
+ const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
922
+ const issueNumber = issueNumberFromUrl(issueUrl);
923
+ const body = [
924
+ issueNumber ? `Closes #${issueNumber}` : "",
925
+ "",
926
+ issueText
927
+ ].join("\n").trim();
928
+ const bodyPath = path.join(paths.sessionRoot, "pr_body.md");
929
+ await writeTextFile(bodyPath, body);
930
+ const result = await runCommand("gh", [
931
+ "pr",
932
+ "create",
933
+ "--title",
934
+ issueTitle,
935
+ "--body-file",
936
+ bodyPath
937
+ ], {
938
+ cwd: paths.worktree,
939
+ timeout: 1000 * 60
940
+ });
941
+ if (!result.ok || !result.stdout) {
942
+ const prompt = await renderPrompt(paths, "pr_failure.md", {
943
+ doctor_output: result.output
944
+ });
945
+ await writePromptArtifact(paths, "pr_create_failure.md", prompt);
946
+ return failSession(paths, {
947
+ code: "pr_create_failed",
948
+ message: result.output || "Failed to create PR.",
949
+ repairCommand: "gh pr create",
950
+ prompt
951
+ });
952
+ }
953
+ const prUrl = result.stdout.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || result.stdout;
954
+ await writeTextFile(path.join(paths.sessionRoot, "pr_url"), prUrl);
955
+ await writeReceipt(paths, "pr_created", `Pushed branch ${paths.branch} and created PR ${prUrl}.`);
956
+ await markStatus(paths, SESSION_STATUS.RUNNING);
957
+ return buildSessionResponse(paths);
958
+ }
959
+
960
+ async function mergePr(paths) {
961
+ const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
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"], {
966
+ cwd: paths.worktree,
967
+ timeout: 1000 * 60 * 5
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`);
989
+ }
990
+ if (await hasWorktree(paths)) {
991
+ const result = await runGit(paths.targetRoot, ["worktree", "remove", paths.worktree], {
992
+ timeout: 1000 * 60
993
+ });
994
+ if (!result.ok) {
995
+ return failSession(paths, {
996
+ code: "worktree_remove_failed",
997
+ message: result.output || "Failed to remove worktree.",
998
+ repairCommand: `git worktree remove ${paths.worktree}`
999
+ });
1000
+ }
1001
+ }
1002
+ await writeReceipt(paths, "pr_merged", `Merged PR ${prUrl} and removed worktree ${paths.worktree}.`);
1003
+ await markStatus(paths, SESSION_STATUS.RUNNING);
1004
+ return buildSessionResponse(paths);
1005
+ }
1006
+
1007
+ async function finishSession(paths) {
1008
+ const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
1009
+ const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
1010
+ const codexThreadId = await readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id"));
1011
+ const prompt = await renderPrompt(paths, "final_comment.md", {
1012
+ codex_thread_id: codexThreadId,
1013
+ issue_url: issueUrl,
1014
+ pr_url: prUrl,
1015
+ transcript_log: path.join(paths.completedSessionRoot, "transcript.log")
1016
+ });
1017
+ await writeTextFile(path.join(paths.sessionRoot, "final_comment.md"), prompt);
1018
+ if (issueUrl) {
1019
+ await runCommand("gh", ["issue", "comment", issueUrl, "--body-file", path.join(paths.sessionRoot, "final_comment.md")], {
1020
+ cwd: paths.targetRoot,
1021
+ timeout: 1000 * 60
1022
+ });
1023
+ }
1024
+ await writeReceipt(paths, "session_finished", `Finished session ${paths.sessionId}.`);
1025
+ await markStatus(paths, SESSION_STATUS.FINISHED);
1026
+ await markCurrentStep(paths, "");
1027
+ const archivedPaths = await archiveSession(paths, "completed");
1028
+ return buildSessionResponse(archivedPaths, {
1029
+ status: SESSION_STATUS.FINISHED
1030
+ });
1031
+ }
1032
+
1033
+ const STEP_RUNNERS = Object.freeze({
1034
+ worktree_created: createWorktree,
1035
+ dependencies_installed: installDependencies,
1036
+ issue_prompt_rendered: renderIssuePrompt,
1037
+ issue_drafted: draftIssue,
1038
+ issue_created: createIssue,
1039
+ plan_made: makePlan,
1040
+ implementation_changes_accepted: acceptImplementationChanges,
1041
+ implementation_changes_committed: commitImplementation,
1042
+ review_prompt_rendered: renderReviewPrompt,
1043
+ review_changes_accepted: acceptReviewChanges,
1044
+ review_changes_committed: commitReviewChanges,
1045
+ user_check_completed: userCheck,
1046
+ doctor_run: runDoctor,
1047
+ pr_created: createPr,
1048
+ pr_merged: mergePr,
1049
+ session_finished: finishSession
1050
+ });
1051
+
1052
+ const PRECONDITION_RUNNERS = Object.freeze({
1053
+ git_current_branch: (paths) => assertGitCurrentBranch(paths.targetRoot),
1054
+ git_repository: (paths) => assertGitRepository(paths.targetRoot),
1055
+ github_auth: (paths) => assertGhAuth(paths.targetRoot),
1056
+ github_origin: (paths) => assertGithubOrigin(paths.targetRoot),
1057
+ issue_text_exists: assertIssueTextExists,
1058
+ issue_url_exists: assertIssueUrlExists,
1059
+ pr_url_exists: assertPrUrlExists,
1060
+ session_exists: assertSessionExists,
1061
+ worktree_exists: assertWorktreeExists
1062
+ });
1063
+
1064
+ async function runNamedPreconditions(paths, names = []) {
1065
+ return applyPreconditions(
1066
+ paths,
1067
+ names.map((name) => {
1068
+ return async () => PRECONDITION_RUNNERS[name](paths);
1069
+ })
1070
+ );
1071
+ }
1072
+
1073
+ async function runSessionStep({
1074
+ targetRoot = process.cwd(),
1075
+ sessionId,
1076
+ options = {}
1077
+ } = {}) {
1078
+ return withExistingSession({ targetRoot, sessionId }, async (paths) => {
1079
+ const artifacts = await readSessionArtifacts(paths);
1080
+ if (artifacts.status === SESSION_STATUS.FINISHED || artifacts.status === SESSION_STATUS.ABANDONED) {
1081
+ return buildSessionResponse(paths, {
1082
+ ok: true,
1083
+ status: artifacts.status
1084
+ });
1085
+ }
1086
+ const nextStep = artifacts.nextStep;
1087
+ if (!nextStep) {
1088
+ return finishSession(paths);
1089
+ }
1090
+ if (nextStep === "session_created") {
1091
+ return failSession(paths, {
1092
+ code: "session_not_initialized",
1093
+ message: "Session exists but is missing its creation receipt.",
1094
+ repairCommand: "jskit session create"
1095
+ });
1096
+ }
1097
+ const runner = STEP_RUNNERS[nextStep];
1098
+ if (typeof runner !== "function") {
1099
+ return failSession(paths, {
1100
+ code: "step_not_implemented",
1101
+ message: `No runner exists for step ${nextStep}.`,
1102
+ status: SESSION_STATUS.FAILED
1103
+ });
1104
+ }
1105
+ const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
1106
+ if (!stepPreconditions.ok) {
1107
+ return failSession(paths, {
1108
+ ...stepPreconditions.error,
1109
+ preconditions: stepPreconditions.preconditions
1110
+ });
1111
+ }
1112
+ return runner(paths, options, {
1113
+ preconditions: stepPreconditions.preconditions
1114
+ });
1115
+ });
1116
+ }
1117
+
1118
+ async function abandonSession({
1119
+ targetRoot = process.cwd(),
1120
+ sessionId
1121
+ } = {}) {
1122
+ return withExistingSession({ targetRoot, sessionId }, async (paths) => {
1123
+ const artifacts = await readSessionArtifacts(paths);
1124
+ if (artifacts.status === SESSION_STATUS.FINISHED || artifacts.status === SESSION_STATUS.ABANDONED) {
1125
+ return buildSessionResponse(paths, {
1126
+ status: artifacts.status
1127
+ });
1128
+ }
1129
+ const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
1130
+ if (issueUrl) {
1131
+ const closeIssueResult = await runCommand("gh", ["issue", "close", issueUrl, "--comment", `Abandoned JSKIT Studio session ${paths.sessionId}.`], {
1132
+ cwd: paths.targetRoot,
1133
+ timeout: 1000 * 60
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
+ }
1143
+ }
1144
+ if (await hasWorktree(paths)) {
1145
+ await runGit(paths.targetRoot, ["worktree", "remove", "--force", paths.worktree], {
1146
+ timeout: 1000 * 60
1147
+ });
1148
+ }
1149
+ await writeTextFile(
1150
+ path.join(paths.sessionRoot, "steps", "abandoned"),
1151
+ `${timestampForReceipt()}\nAbandoned session ${paths.sessionId}.`
1152
+ );
1153
+ await markStatus(paths, SESSION_STATUS.ABANDONED);
1154
+ await markCurrentStep(paths, "");
1155
+ const archivedPaths = await archiveSession(paths, "abandoned");
1156
+ return buildSessionResponse(archivedPaths, {
1157
+ status: SESSION_STATUS.ABANDONED
1158
+ });
1159
+ });
1160
+ }
1161
+
1162
+ async function adoptCodexThreadId({
1163
+ targetRoot = process.cwd(),
1164
+ sessionId,
1165
+ codexThreadId
1166
+ } = {}) {
1167
+ if (!isValidSessionId(sessionId)) {
1168
+ return invalidSessionIdResponse({ targetRoot, sessionId });
1169
+ }
1170
+ const normalizedThreadId = normalizeText(codexThreadId);
1171
+ if (!normalizedThreadId) {
1172
+ return failSession(resolveSessionPaths({ targetRoot, sessionId }), {
1173
+ code: "codex_thread_id_required",
1174
+ message: "Codex thread id is required."
1175
+ });
1176
+ }
1177
+ return withExistingSession({ targetRoot, sessionId }, async (paths) => {
1178
+ if (paths.archive && paths.archive !== "active") {
1179
+ return buildSessionResponse(paths, {
1180
+ ok: false,
1181
+ errors: [
1182
+ createError({
1183
+ code: "session_archived_read_only",
1184
+ message: `Session ${paths.sessionId} is archived and cannot be mutated.`
1185
+ })
1186
+ ]
1187
+ });
1188
+ }
1189
+ await writeTextFile(path.join(paths.sessionRoot, "codex_thread_id"), normalizedThreadId);
1190
+ return buildSessionResponse(paths);
1191
+ });
1192
+ }
1193
+
1194
+ export {
1195
+ SESSION_STATUS,
1196
+ STEP_DEFINITIONS,
1197
+ STEP_IDS,
1198
+ STEP_PRECONDITION_NAMES,
1199
+ abandonSession,
1200
+ adoptDependenciesInstalled,
1201
+ adoptCodexThreadId,
1202
+ buildSessionResponse,
1203
+ buildSessionErrorResponse,
1204
+ createSession,
1205
+ createSessionId,
1206
+ extractIssueTitle,
1207
+ extractIssueText,
1208
+ extractPlanText,
1209
+ inspectSession,
1210
+ inspectSessionDiff,
1211
+ inspectSessionDetails,
1212
+ isValidSessionId,
1213
+ listSessions,
1214
+ renderTemplate,
1215
+ recordDependenciesInstalled,
1216
+ resolveSessionPaths,
1217
+ runSessionStep
1218
+ };